mirror of
https://github.com/corda/corda.git
synced 2024-12-21 05:53:23 +00:00
Merge branch 'master' into arri_update_collectSignaturesFlow_Docs
This commit is contained in:
commit
4deea43b98
@ -9,6 +9,7 @@ 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
|
||||
@ -71,6 +72,7 @@ class CordaRPCClient(
|
||||
try {
|
||||
SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply {
|
||||
registerScheme(KryoClientSerializationScheme())
|
||||
registerScheme(AMQPClientSerializationScheme())
|
||||
}
|
||||
SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT
|
||||
SerializationDefaults.RPC_CLIENT_CONTEXT = KRYO_RPC_CLIENT_CONTEXT
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
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
|
||||
@ -211,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: LedgerTransaction,
|
||||
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
|
||||
|
||||
|
@ -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.transactions.LedgerTransaction
|
||||
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: LedgerTransaction,
|
||||
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.transactions.LedgerTransaction
|
||||
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: LedgerTransaction, 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,73 +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.transactions.LedgerTransaction
|
||||
import net.corda.core.utilities.loggerFor
|
||||
|
||||
/**
|
||||
* 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 = 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
|
||||
* [LedgerTransaction.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: LedgerTransaction,
|
||||
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.transactions.LedgerTransaction
|
||||
|
||||
/**
|
||||
* 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: LedgerTransaction,
|
||||
clause: Clause<ContractState, C, Unit>,
|
||||
commands: List<AuthenticatedObject<C>>) {
|
||||
if (Clause.log.isTraceEnabled) {
|
||||
clause.getExecutionPath(commands).forEach {
|
||||
Clause.log.trace("Tx ${tx.id} clause: $clause")
|
||||
}
|
||||
}
|
||||
val matchedCommands = clause.verify(tx, tx.inputStates, tx.outputStates, 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.transactions.LedgerTransaction
|
||||
|
||||
/**
|
||||
* 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: LedgerTransaction,
|
||||
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.transactions.LedgerTransaction
|
||||
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: LedgerTransaction, 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.transactions.LedgerTransaction
|
||||
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: LedgerTransaction, 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.transactions.LedgerTransaction
|
||||
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: LedgerTransaction): List<LedgerTransaction.InOutGroup<S, K>>
|
||||
|
||||
override fun getExecutionPath(commands: List<AuthenticatedObject<C>>): List<Clause<*, *, *>>
|
||||
= clause.getExecutionPath(commands)
|
||||
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
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
|
||||
}
|
||||
}
|
@ -14,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
|
||||
@ -40,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
|
||||
@ -53,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
|
||||
|
||||
@ -196,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.
|
||||
@ -770,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) }
|
||||
|
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)
|
@ -5,18 +5,13 @@ import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
@ -224,29 +219,7 @@ interface VaultService {
|
||||
|
||||
fun getTransactionNotes(txnId: SecureHash): Iterable<String>
|
||||
|
||||
/**
|
||||
* Generate a transaction that moves an amount of currency to the given pubkey.
|
||||
*
|
||||
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
|
||||
*
|
||||
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
|
||||
* to move the cash will be added on top.
|
||||
* @param amount How much currency to send.
|
||||
* @param to a key of the recipient.
|
||||
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
|
||||
* of given parties. This can be useful if the party you're trying to pay has expectations
|
||||
* about which type of asset claims they are willing to accept.
|
||||
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
|
||||
* the resulting transaction for it to be valid.
|
||||
* @throws InsufficientBalanceException when a cash spending transaction fails because
|
||||
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
|
||||
*/
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
@Suspendable
|
||||
fun generateSpend(tx: TransactionBuilder,
|
||||
amount: Amount<Currency>,
|
||||
to: AbstractParty,
|
||||
onlyFromParties: Set<AbstractParty>? = null): Pair<TransactionBuilder, List<PublicKey>>
|
||||
// DOCEND VaultStatesQuery
|
||||
|
||||
/**
|
||||
* Soft locking is used to prevent multiple transactions trying to use the same output simultaneously.
|
||||
@ -257,7 +230,10 @@ interface VaultService {
|
||||
|
||||
/**
|
||||
* Reserve a set of [StateRef] for a given [UUID] unique identifier.
|
||||
* Typically, the unique identifier will refer to a Flow lockId associated with a [Transaction] in an in-flight flow.
|
||||
* Typically, the unique identifier will refer to a [FlowLogic.runId.uuid] associated with an in-flight flow.
|
||||
* In this case if the flow terminates the locks will automatically be freed, even if there is an error.
|
||||
* However, the user can specify their own [UUID] and manage this manually, possibly across the lifetime of multiple flows,
|
||||
* or from other thread contexts e.g. [CordaService] instances.
|
||||
* In the case of coin selection, soft locks are automatically taken upon gathering relevant unconsumed input refs.
|
||||
*
|
||||
* @throws [StatesNotAvailableException] when not possible to softLock all of requested [StateRef]
|
||||
@ -273,21 +249,32 @@ interface VaultService {
|
||||
* are consumed as part of cash spending.
|
||||
*/
|
||||
fun softLockRelease(lockId: UUID, stateRefs: NonEmptySet<StateRef>? = null)
|
||||
|
||||
// DOCEND SoftLockAPI
|
||||
|
||||
/**
|
||||
* TODO: this function should be private to the vault, but currently Cash Exit functionality
|
||||
* is implemented in a separate module (finance) and requires access to it.
|
||||
* Helper function to combine using [VaultQueryService] calls to determine spendable states and soft locking them.
|
||||
* Currently performance will be worse than for the hand optimised version in `Cash.unconsumedCashStatesForSpending`
|
||||
* However, this is fully generic and can operate with custom [FungibleAsset] states.
|
||||
* @param lockId The [FlowLogic.runId.uuid] of the current flow used to soft lock the states.
|
||||
* @param eligibleStatesQuery A custom query object that selects down to the appropriate subset of all states of the
|
||||
* [contractType]. e.g. by selecting on account, issuer, etc. The query is internally augmented with the UNCONSUMED,
|
||||
* soft lock and contract type requirements.
|
||||
* @param amount The required amount of the asset, but with the issuer stripped off.
|
||||
* It is assumed that compatible issuer states will be filtered out by the [eligibleStatesQuery].
|
||||
* @param contractType class type of the result set.
|
||||
* @return Returns a locked subset of the [eligibleStatesQuery] sufficient to satisfy the requested amount,
|
||||
* or else an empty list and no change in the stored lock states when their are insufficient resources available.
|
||||
*/
|
||||
@Suspendable
|
||||
fun <T : ContractState> unconsumedStatesForSpending(amount: Amount<Currency>,
|
||||
onlyFromIssuerParties: Set<AbstractParty>? = null,
|
||||
notary: Party? = null,
|
||||
lockId: UUID,
|
||||
withIssuerRefs: Set<OpaqueBytes>? = null): List<StateAndRef<T>>
|
||||
@Throws(StatesNotAvailableException::class)
|
||||
fun <T : FungibleAsset<U>, U : Any> tryLockFungibleStatesForSpending(lockId: UUID,
|
||||
eligibleStatesQuery: QueryCriteria,
|
||||
amount: Amount<U>,
|
||||
contractType: Class<out T>): List<StateAndRef<T>>
|
||||
|
||||
}
|
||||
|
||||
|
||||
class StatesNotAvailableException(override val message: String?, override val cause: Throwable? = null) : FlowException(message, cause) {
|
||||
override fun toString() = "Soft locking error: $message"
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package net.corda.core.schemas.converters
|
||||
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import javax.persistence.AttributeConverter
|
||||
import javax.persistence.Converter
|
||||
|
||||
/**
|
||||
* Converter to persist a party as its's well known identity (where resolvable)
|
||||
* Completely anonymous parties are stored as null (to preserve privacy)
|
||||
*/
|
||||
@Converter(autoApply = true)
|
||||
class AbstractPartyToX500NameAsStringConverter(val identitySvc: IdentityService) : AttributeConverter<AbstractParty, String> {
|
||||
|
||||
override fun convertToDatabaseColumn(party: AbstractParty?): String? {
|
||||
party?.let {
|
||||
return identitySvc.partyFromAnonymous(party)?.toString()
|
||||
}
|
||||
return null // non resolvable anonymous parties
|
||||
}
|
||||
|
||||
override fun convertToEntityAttribute(dbData: String?): AbstractParty? {
|
||||
dbData?.let {
|
||||
val party = identitySvc.partyFromX500Name(X500Name(dbData))
|
||||
return party as AbstractParty
|
||||
}
|
||||
return null // non resolvable anonymous parties are stored as nulls
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.transactions.SignedTransaction.SignaturesMissingException
|
||||
import net.corda.core.utilities.toNonEmptySet
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
|
||||
@ -52,9 +53,10 @@ interface TransactionWithSignatures : NamedByHash {
|
||||
* corrupt. If you use this function directly you'll need to do the other checks yourself. Probably you
|
||||
* want [verifySignatures] instead.
|
||||
*
|
||||
* @throws InvalidKeyException if the key on a signature is invalid.
|
||||
* @throws SignatureException if a signature fails to verify.
|
||||
*/
|
||||
@Throws(SignatureException::class)
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun checkSignaturesAreValid() {
|
||||
for (sig in sigs) {
|
||||
sig.verify(id)
|
||||
|
@ -1,32 +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.PrivacySalt
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class AllOfTests {
|
||||
|
||||
@Test
|
||||
fun minimal() {
|
||||
val counter = AtomicInteger(0)
|
||||
val clause = AllOf(matchedClause(counter), matchedClause(counter))
|
||||
val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, null, PrivacySalt())
|
||||
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
|
||||
|
||||
// Check that we've run the verify() function of two clauses
|
||||
assertEquals(2, counter.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `not all match`() {
|
||||
val clause = AllOf(matchedClause(), unmatchedClause())
|
||||
val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, null, PrivacySalt())
|
||||
assertFailsWith<IllegalStateException> { verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>()) }
|
||||
}
|
||||
}
|
@ -1,45 +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.PrivacySalt
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class AnyOfTests {
|
||||
@Test
|
||||
fun minimal() {
|
||||
val counter = AtomicInteger(0)
|
||||
val clause = AnyOf(matchedClause(counter), matchedClause(counter))
|
||||
val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, null, PrivacySalt())
|
||||
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
|
||||
|
||||
// Check that we've run the verify() function of two clauses
|
||||
assertEquals(2, counter.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `not all match`() {
|
||||
val counter = AtomicInteger(0)
|
||||
val clause = AnyOf(matchedClause(counter), unmatchedClause(counter))
|
||||
val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, null, PrivacySalt())
|
||||
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
|
||||
|
||||
// Check that we've run the verify() function of one clause
|
||||
assertEquals(1, counter.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `none match`() {
|
||||
val counter = AtomicInteger(0)
|
||||
val clause = AnyOf(unmatchedClause(counter), unmatchedClause(counter))
|
||||
val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, null, PrivacySalt())
|
||||
assertFailsWith(IllegalArgumentException::class) {
|
||||
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
|
||||
}
|
||||
}
|
||||
}
|
@ -1,30 +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.transactions.LedgerTransaction
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
internal fun matchedClause(counter: AtomicInteger? = null) = object : Clause<ContractState, CommandData, Unit>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = emptySet()
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
commands: List<AuthenticatedObject<CommandData>>, groupingKey: Unit?): Set<CommandData> {
|
||||
counter?.incrementAndGet()
|
||||
return emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
/** A clause that can never be matched */
|
||||
internal fun unmatchedClause(counter: AtomicInteger? = null) = object : Clause<ContractState, CommandData, Unit>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(object : CommandData {}.javaClass)
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
commands: List<AuthenticatedObject<CommandData>>, groupingKey: Unit?): Set<CommandData> {
|
||||
counter?.incrementAndGet()
|
||||
return emptySet()
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
package net.corda.core.contracts.clauses
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
/**
|
||||
* Tests for the clause verifier.
|
||||
*/
|
||||
class VerifyClausesTests {
|
||||
/** Very simple check that the function doesn't error when given any clause */
|
||||
@Test
|
||||
fun minimal() {
|
||||
val clause = object : Clause<ContractState, CommandData, Unit>() {
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
commands: List<AuthenticatedObject<CommandData>>, groupingKey: Unit?): Set<CommandData> = emptySet()
|
||||
}
|
||||
val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, null, PrivacySalt())
|
||||
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun errorSuperfluousCommands() {
|
||||
val clause = object : Clause<ContractState, CommandData, Unit>() {
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
commands: List<AuthenticatedObject<CommandData>>, groupingKey: Unit?): Set<CommandData> = emptySet()
|
||||
}
|
||||
val command = AuthenticatedObject(emptyList(), emptyList(), DummyContract.Commands.Create())
|
||||
val tx = LedgerTransaction(emptyList(), emptyList(), listOf(command), emptyList(), SecureHash.randomSHA256(), null, null, PrivacySalt())
|
||||
// The clause is matched, but doesn't mark the command as consumed, so this should error
|
||||
assertFailsWith<IllegalStateException> { verifyClause(tx, clause, listOf(command)) }
|
||||
}
|
||||
}
|
@ -8,9 +8,7 @@ import net.corda.core.internal.declaredField
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.node.utilities.loadKeyStore
|
||||
import net.corda.node.utilities.loadOrCreateKeyStore
|
||||
import net.corda.node.utilities.save
|
||||
import net.corda.node.utilities.*
|
||||
import net.corda.testing.TestDependencyInjectionBase
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.junit.Rule
|
||||
|
@ -1,9 +1,7 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.internal.toTypedArray
|
||||
import net.corda.node.utilities.KEYSTORE_TYPE
|
||||
import net.corda.node.utilities.addOrReplaceCertificate
|
||||
import net.corda.node.utilities.addOrReplaceKey
|
||||
import net.corda.node.utilities.*
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.GeneralName
|
||||
import org.bouncycastle.asn1.x509.GeneralSubtree
|
||||
@ -20,13 +18,13 @@ class X509NameConstraintsTest {
|
||||
|
||||
private fun makeKeyStores(subjectName: X500Name, nameConstraints: NameConstraints): Pair<KeyStore, KeyStore> {
|
||||
val rootKeys = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val rootCACert = X509Utilities.createSelfSignedCACertificate(X509Utilities.getX509Name("Corda Root CA","London","demo@r3.com",null), rootKeys)
|
||||
val rootCACert = X509Utilities.createSelfSignedCACertificate(getX509Name("Corda Root CA", "London", "demo@r3.com", null), rootKeys)
|
||||
|
||||
val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootKeys, X509Utilities.getX509Name("Corda Intermediate CA","London","demo@r3.com",null), intermediateCAKeyPair.public)
|
||||
val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootKeys, getX509Name("Corda Intermediate CA", "London", "demo@r3.com", null), intermediateCAKeyPair.public)
|
||||
|
||||
val clientCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKeyPair, X509Utilities.getX509Name("Corda Client CA","London","demo@r3.com",null), clientCAKeyPair.public, nameConstraints = nameConstraints)
|
||||
val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKeyPair, getX509Name("Corda Client CA", "London", "demo@r3.com", null), clientCAKeyPair.public, nameConstraints = nameConstraints)
|
||||
|
||||
val keyPass = "password"
|
||||
val trustStore = KeyStore.getInstance(KEYSTORE_TYPE)
|
||||
|
@ -93,6 +93,13 @@ Several examples of entities and mappings are provided in the codebase, includin
|
||||
.. literalinclude:: ../../finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt
|
||||
:language: kotlin
|
||||
|
||||
Identity mapping
|
||||
----------------
|
||||
Schema entity attributes defined by identity types (``AbstractParty``, ``Party``, ``AnonymousParty``) are automatically
|
||||
processed to ensure only the ``X500Name`` of the identity is persisted where an identity is well known, otherwise a null
|
||||
value is stored in the associated column. To preserve privacy, identity keys are never persisted. Developers should use
|
||||
the ``IdentityService`` to resolve keys from well know X500 identity names.
|
||||
|
||||
JDBC session
|
||||
------------
|
||||
Apps may also interact directly with the underlying Node's database by using a standard
|
||||
|
@ -7,8 +7,36 @@ from the previous milestone release.
|
||||
UNRELEASED
|
||||
----------
|
||||
|
||||
* The concept of ``TransactionType`` has been removed. Transactions no longer carry a `type` property. All usages of
|
||||
``TransactionType.General.Builder()`` have to be replaced with ``TransactionBuilder()``.
|
||||
* Vault Query fix: filter by multiple issuer names in ``FungibleAssetQueryCriteria``
|
||||
|
||||
* Following deprecated methods have been removed:
|
||||
* In ``DataFeed``
|
||||
* ``first`` and ``current``, replaced by ``snapshot``
|
||||
* ``second`` and ``future``, replaced by ``updates``
|
||||
* In ``CordaRPCOps``
|
||||
* ``stateMachinesAndUpdates``, replaced by ``stateMachinesFeed``
|
||||
* ``verifiedTransactions``, replaced by ``verifiedTransactionsFeed``
|
||||
* ``stateMachineRecordedTransactionMapping``, replaced by ``stateMachineRecordedTransactionMappingFeed``
|
||||
* ``networkMapUpdates``, replaced by ``networkMapFeed``
|
||||
|
||||
* Due to security concerns and the need to remove the concept of state relevancy (which isn't needed in Corda),
|
||||
``ResolveTransactionsFlow`` has been made internal. Instead merge the receipt of the ``SignedTransaction`` and the subsequent
|
||||
sub-flow call to ``ResolveTransactionsFlow`` with a single call to ``ReceiveTransactionFlow``. The flow running on the counterparty
|
||||
must use ``SendTransactionFlow`` at the correct place. There is also ``ReceiveStateAndRefFlow`` and ``SendStateAndRefFlow`` for
|
||||
dealing with ``StateAndRef``s.
|
||||
|
||||
|
||||
* Vault query soft locking enhancements and deprecations
|
||||
* removed original ``VaultService`` ``softLockedStates` query mechanism.
|
||||
* introduced improved ``SoftLockingCondition`` filterable attribute in ``VaultQueryCriteria`` to enable specification
|
||||
of different soft locking retrieval behaviours (exclusive of soft locked states, soft locked states only, specified
|
||||
by set of lock ids)
|
||||
|
||||
* Trader demo now issues cash and commercial paper directly from the bank node, rather than the seller node self-issuing
|
||||
commercial paper but labelling it as if issued by the bank.
|
||||
|
||||
Milestone 14
|
||||
------------
|
||||
|
||||
* Changes in ``NodeInfo``:
|
||||
|
||||
@ -37,7 +65,7 @@ UNRELEASED
|
||||
* Moved the core flows previously found in ``net.corda.flows`` into ``net.corda.core.flows``. This is so that all packages
|
||||
in the ``core`` module begin with ``net.corda.core``.
|
||||
|
||||
* ``FinalityFlow`` now has can be subclassed, and the ``broadcastTransaction`` and ``lookupParties`` function can be
|
||||
* ``FinalityFlow`` can now be subclassed, and the ``broadcastTransaction`` and ``lookupParties`` function can be
|
||||
overriden in order to handle cases where no single transaction participant is aware of all parties, and therefore
|
||||
the transaction must be relayed between participants rather than sent from a single node.
|
||||
|
||||
@ -60,32 +88,45 @@ UNRELEASED
|
||||
* ``Cordformation`` adds a ``corda`` and ``cordaRuntime`` configuration to projects which cordapp developers should
|
||||
use to exclude core Corda JARs from being built into Cordapp fat JARs.
|
||||
|
||||
.. Milestone 15:
|
||||
* ``database`` field in ``AbstractNode`` class has changed the type from ``org.jetbrains.exposed.sql.Database`` to
|
||||
‘net.corda.node.utilities.CordaPersistence’ - no change is needed for the typical use
|
||||
(i.e. services.database.transaction { code block } ) however a change is required when Database was explicitly declared
|
||||
|
||||
* Vault Query fix: filter by multiple issuer names in ``FungibleAssetQueryCriteria``
|
||||
* ``DigitalSignature.LegallyIdentifiable``, previously used to identify a signer (e.g. in Oracles), has been removed.
|
||||
One can use the public key to derive the corresponding identity.
|
||||
|
||||
* Following deprecated methods have been removed:
|
||||
* In ``DataFeed``
|
||||
* ``first`` and ``current``, replaced by ``snapshot``
|
||||
* ``second`` and ``future``, replaced by ``updates``
|
||||
* In ``CordaRPCOps``
|
||||
* ``stateMachinesAndUpdates``, replaced by ``stateMachinesFeed``
|
||||
* ``verifiedTransactions``, replaced by ``verifiedTransactionsFeed``
|
||||
* ``stateMachineRecordedTransactionMapping``, replaced by ``stateMachineRecordedTransactionMappingFeed``
|
||||
* ``networkMapUpdates``, replaced by ``networkMapFeed``
|
||||
* Vault Query improvements and fixes:
|
||||
|
||||
* Due to security concerns and the need to remove the concept of state relevancy (which isn't needed in Corda),
|
||||
``ResolveTransactionsFlow`` has been made internal. Instead merge the receipt of the ``SignedTransaction`` and the subsequent
|
||||
sub-flow call to ``ResolveTransactionsFlow`` with a single call to ``ReceiveTransactionFlow``. The flow running on the counterparty
|
||||
must use ``SendTransactionFlow`` at the correct place. There is also ``ReceiveStateAndRefFlow`` and ``SendStateAndRefFlow`` for
|
||||
dealing with ``StateAndRef``s.
|
||||
* FIX inconsistent behaviour: Vault Query defaults to UNCONSUMED in all QueryCriteria types
|
||||
|
||||
* FIX serialization error: Vault Query over RPC when using custom attributes using VaultCustomQueryCriteria.
|
||||
|
||||
* Vault query soft locking enhancements and deprecations
|
||||
* removed original ``VaultService`` ``softLockedStates` query mechanism.
|
||||
* introduced improved ``SoftLockingCondition`` filterable attribute in ``VaultQueryCriteria`` to enable specification
|
||||
of different soft locking retrieval behaviours (exclusive of soft locked states, soft locked states only, specified
|
||||
by set of lock ids)
|
||||
* Aggregate function support: extended VaultCustomQueryCriteria and associated DSL to enable specification of
|
||||
Aggregate Functions (sum, max, min, avg, count) with, optional, group by clauses and sorting (on calculated aggregate)
|
||||
|
||||
* Pagination simplification
|
||||
Pagination continues to be optional, but with following changes:
|
||||
- If no PageSpecification provided then a maximum of MAX_PAGE_SIZE (200) results will be returned, otherwise we fail-fast with a ``VaultQueryException`` to alert the API user to the need to specify a PageSpecification.
|
||||
Internally, we no longer need to calculate a results count (thus eliminating an expensive SQL query) unless a PageSpecification is supplied (note: that a value of -1 is returned for total_results in this scenario).
|
||||
Internally, we now use the AggregateFunction capability to perform the count.
|
||||
- Paging now starts from 1 (was previously 0).
|
||||
|
||||
* Additional Sort criteria: by StateRef (or constituents: txId, index)
|
||||
|
||||
* Confidential identities API improvements
|
||||
|
||||
* Registering anonymous identities now takes in AnonymousPartyAndPath
|
||||
* AnonymousParty.toString() now uses toStringShort() to match other toString() functions
|
||||
* Add verifyAnonymousIdentity() function to verify without storing an identity
|
||||
* Replace pathForAnonymous() with anonymousFromKey() which matches actual use-cases better
|
||||
* Add unit test for fetching the anonymous identity from a key
|
||||
* Update verifyAnonymousIdentity() function signature to match registerAnonymousIdentity()
|
||||
* Rename AnonymisedIdentity to AnonymousPartyAndPath
|
||||
* Remove certificate from AnonymousPartyAndPath as it's not actually used.
|
||||
* Rename registerAnonymousIdentity() to verifyAndRegisterAnonymousIdentity()
|
||||
|
||||
* Added JPA ``AbstractPartyConverter`` to ensure identity schema attributes are persisted securely according to type
|
||||
(well known party, resolvable anonymous party, completely anonymous party).
|
||||
|
||||
Milestone 13
|
||||
------------
|
||||
|
@ -26,6 +26,7 @@ import net.corda.testing.contracts.DummyState;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SignatureException;
|
||||
import java.time.Duration;
|
||||
@ -506,7 +507,7 @@ public class FlowCookbookJava {
|
||||
twiceSignedTx.checkSignaturesAreValid();
|
||||
// DOCEND 37
|
||||
|
||||
} catch (SignatureException e) {
|
||||
} catch (GeneralSecurityException e) {
|
||||
// Handle this as required.
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.Issued
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.withoutIssuer
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.flows.FlowLogic
|
||||
@ -13,7 +14,6 @@ import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.node.services.vault.builder
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
@ -31,9 +31,10 @@ private data class FxRequest(val tradeId: String,
|
||||
val notary: Party? = null)
|
||||
|
||||
// DOCSTART 1
|
||||
// This is equivalent to the VaultService.generateSpend
|
||||
// This is equivalent to the Cash.generateSpend
|
||||
// Which is brought here to make the filtering logic more visible in the example
|
||||
private fun gatherOurInputs(serviceHub: ServiceHub,
|
||||
lockId: UUID,
|
||||
amountRequired: Amount<Issued<Currency>>,
|
||||
notary: Party?): Pair<List<StateAndRef<Cash.State>>, Long> {
|
||||
// extract our identity for convenience
|
||||
@ -41,34 +42,25 @@ private fun gatherOurInputs(serviceHub: ServiceHub,
|
||||
val ourParties = ourKeys.map { serviceHub.identityService.partyFromKey(it) ?: throw IllegalStateException("Unable to resolve party from key") }
|
||||
val fungibleCriteria = QueryCriteria.FungibleAssetQueryCriteria(owner = ourParties)
|
||||
|
||||
val notaryName = if (notary != null) notary.name else serviceHub.networkMapCache.getAnyNotary()!!.name
|
||||
val vaultCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notaryName = listOf(notaryName))
|
||||
|
||||
val logicalExpression = builder { CashSchemaV1.PersistentCashState::currency.equal(amountRequired.token.product.currencyCode) }
|
||||
val cashCriteria = QueryCriteria.VaultCustomQueryCriteria(logicalExpression)
|
||||
|
||||
// Collect cash type inputs
|
||||
val suitableCashStates = serviceHub.vaultQueryService.queryBy<Cash.State>(fungibleCriteria.and(cashCriteria)).states
|
||||
require(!suitableCashStates.isEmpty()) { "Insufficient funds" }
|
||||
val fullCriteria = fungibleCriteria.and(vaultCriteria).and(cashCriteria)
|
||||
|
||||
var remaining = amountRequired.quantity
|
||||
// We will need all of the inputs to be on the same notary.
|
||||
// For simplicity we just filter on the first notary encountered
|
||||
// A production quality flow would need to migrate notary if the
|
||||
// the amounts were not sufficient in any one notary
|
||||
val sourceNotary: Party = notary ?: suitableCashStates.first().state.notary
|
||||
val eligibleStates = serviceHub.vaultService.tryLockFungibleStatesForSpending<Cash.State, Currency>(lockId, fullCriteria, amountRequired.withoutIssuer(), Cash.State::class.java)
|
||||
|
||||
val inputsList = mutableListOf<StateAndRef<Cash.State>>()
|
||||
// Iterate over filtered cash states to gather enough to pay
|
||||
for (cash in suitableCashStates.filter { it.state.notary == sourceNotary }) {
|
||||
inputsList += cash
|
||||
if (remaining <= cash.state.data.amount.quantity) {
|
||||
return Pair(inputsList, cash.state.data.amount.quantity - remaining)
|
||||
}
|
||||
remaining -= cash.state.data.amount.quantity
|
||||
}
|
||||
throw IllegalStateException("Insufficient funds")
|
||||
check(eligibleStates.isNotEmpty()) { "Insufficient funds" }
|
||||
val amount = eligibleStates.fold(0L) { tot, x -> tot + x.state.data.amount.quantity }
|
||||
val change = amount - amountRequired.quantity
|
||||
|
||||
return Pair(eligibleStates, change)
|
||||
}
|
||||
// DOCEND 1
|
||||
|
||||
private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, request: FxRequest): Pair<List<StateAndRef<Cash.State>>, List<Cash.State>> {
|
||||
private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, lockId: UUID, request: FxRequest): Pair<List<StateAndRef<Cash.State>>, List<Cash.State>> {
|
||||
// Create amount with correct issuer details
|
||||
val sellAmount = request.amount
|
||||
|
||||
@ -78,7 +70,7 @@ private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, request: FxReques
|
||||
// we will use query manually in the helper function below.
|
||||
// Putting this into a non-suspendable function also prevents issues when
|
||||
// the flow is suspended.
|
||||
val (inputs, residual) = gatherOurInputs(serviceHub, sellAmount, request.notary)
|
||||
val (inputs, residual) = gatherOurInputs(serviceHub, lockId, sellAmount, request.notary)
|
||||
|
||||
// Build and an output state for the counterparty
|
||||
val transferedFundsOutput = Cash.State(sellAmount, request.counterparty)
|
||||
@ -119,7 +111,7 @@ class ForeignExchangeFlow(val tradeId: String,
|
||||
} else throw IllegalArgumentException("Our identity must be one of the parties in the trade.")
|
||||
|
||||
// Call the helper method to identify suitable inputs and make the outputs
|
||||
val (outInputStates, ourOutputStates) = prepareOurInputsAndOutputs(serviceHub, localRequest)
|
||||
val (outInputStates, ourOutputStates) = prepareOurInputsAndOutputs(serviceHub, runId.uuid, localRequest)
|
||||
|
||||
// identify the notary for our states
|
||||
val notary = outInputStates.first().state.notary
|
||||
@ -231,7 +223,7 @@ class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic<Unit>() {
|
||||
// we will use query manually in the helper function below.
|
||||
// Putting this into a non-suspendable function also prevent issues when
|
||||
// the flow is suspended.
|
||||
val (ourInputState, ourOutputState) = prepareOurInputsAndOutputs(serviceHub, request)
|
||||
val (ourInputState, ourOutputState) = prepareOurInputsAndOutputs(serviceHub, runId.uuid, request)
|
||||
|
||||
// Send back our proposed states and await the full transaction to verify
|
||||
val ourKey = serviceHub.keyManagementService.filterMyKeys(ourInputState.flatMap { it.state.data.participants }.map { it.owningKey }).single()
|
||||
|
@ -279,7 +279,7 @@ This code is longer but no more complicated. Here are some things to pay attenti
|
||||
|
||||
1. We do some sanity checking on the proposed trade transaction received from the seller to ensure we're being offered
|
||||
what we expected to be offered.
|
||||
2. We create a cash spend using ``VaultService.generateSpend``. You can read the vault documentation to learn more about this.
|
||||
2. We create a cash spend using ``Cash.generateSpend``. You can read the vault documentation to learn more about this.
|
||||
3. We access the *service hub* as needed to access things that are transient and may change or be recreated
|
||||
whilst a flow is suspended, such as the wallet or the network map.
|
||||
4. We call ``CollectSignaturesFlow`` as a subflow to send the unfinished, still-invalid transaction to the seller so
|
||||
|
@ -78,8 +78,8 @@ define an ``IOUState``:
|
||||
|
||||
class IOUState(val value: Int,
|
||||
val lender: Party,
|
||||
val borrower: Party) : ContractState {
|
||||
override val contract: IOUContract = IOUContract()
|
||||
val borrower: Party,
|
||||
override val contract: TemplateContract) : ContractState {
|
||||
|
||||
override val participants get() = listOf(lender, borrower)
|
||||
}
|
||||
@ -153,4 +153,4 @@ We've defined an ``IOUState`` that can be used to represent IOUs as shared facts
|
||||
Corda are simply JVM classes that implement the ``ContractState`` interface. They can have any additional properties and
|
||||
methods you like.
|
||||
|
||||
Next, we'll be writing our ``IOUContract`` to control the evolution of these shared facts over time.
|
||||
Next, we'll be writing our ``IOUContract`` to control the evolution of these shared facts over time.
|
||||
|
@ -6,9 +6,45 @@ Here are release notes for each snapshot release from M9 onwards.
|
||||
Unreleased
|
||||
----------
|
||||
|
||||
The transaction finalisation flow (``FinalityFlow``) has had hooks for alternative implementations, for example in
|
||||
Milestone 14
|
||||
------------
|
||||
|
||||
This release continues with the goal to improve API stability and developer friendliness. There have also been more
|
||||
bug fixes and other improvements across the board.
|
||||
|
||||
The CorDapp template repository has been replaced with a specific repository for
|
||||
`Java <https://github.com/corda/cordapp-template-java>`_ and `Kotlin <https://github.com/corda/cordapp-template-kotlin>`_
|
||||
to improve the experience of starting a new project and to simplify the build system.
|
||||
|
||||
It is now possible to specify multiple IP addresses and legal identities for a single node, allowing node operators
|
||||
more flexibility in setting up nodes.
|
||||
|
||||
A format has been introduced for CorDapp JARs that standardises the contents of CorDapps across nodes. This new format
|
||||
now requires CorDapps to contain their own external dependencies. This paves the way for significantly improved
|
||||
dependency management for CorDapps with the release of `Jigsaw (Java Modules) <http://openjdk.java.net/projects/jigsaw/>`_. For those using non-gradle build systems it is important
|
||||
to read :doc:`cordapp-build-systems` to learn more. Those using our ``cordformation`` plugin simply need to update
|
||||
to the latest version (``0.14.0``) to get the fixes.
|
||||
|
||||
We've now begun the process of demarcating which classes are part of our public API and which ones are internal.
|
||||
Everything found in ``net.corda.core.internal`` and other packages in the ``net.corda`` namespace which has ``.internal`` in it are
|
||||
considered internal and not for public use. In a future release any CorDapp using these packages will fail to load, and
|
||||
when we migrate to Jigsaw these will not be exported.
|
||||
|
||||
The transaction finalisation flow (``FinalityFlow``) has had hooks added for alternative implementations, for example in
|
||||
scenarios where no single participant in a transaction is aware of the well known identities of all parties.
|
||||
|
||||
DemoBench has a fix for a rare but inconvenient crash that can occur when sharing your display across multiple devices,
|
||||
e.g. a projector while performing demonstrations in front of an audience.
|
||||
|
||||
Guava types are being removed because Guava does not have backwards compatibility across versions, which has serious
|
||||
issues when multiple libraries depend on different versions of the library.
|
||||
|
||||
The identity service API has been tweaked, primarily so anonymous identity registration now takes in
|
||||
AnonymousPartyAndPath rather than the individual components of the identity, as typically the caller will have
|
||||
an AnonymousPartyAndPath instance. See change log for further detail.
|
||||
|
||||
Upgrading to this release is strongly recommended in order to keep up with the API changes, removal and additions.
|
||||
|
||||
Milestone 13
|
||||
------------
|
||||
|
||||
|
@ -32,7 +32,7 @@ To run from the command line in Unix:
|
||||
|
||||
1. Run ``./gradlew samples:trader-demo:deployNodes`` to create a set of configs and installs under ``samples/trader-demo/build/nodes``
|
||||
2. Run ``./samples/trader-demo/build/nodes/runnodes`` to open up four new terminals with the four nodes
|
||||
3. Run ``./gradlew samples:trader-demo:runBuyer`` to instruct the buyer node to request issuance of some cash from the Bank of Corda node
|
||||
3. Run ``./gradlew samples:trader-demo:runBank`` to instruct the bank node to issue cash and commercial paper to the buyer and seller nodes respectively.
|
||||
4. Run ``./gradlew samples:trader-demo:runSeller`` to trigger the transaction. If you entered ``flow watch``
|
||||
you can see flows running on both sides of transaction. Additionally you should see final trade information displayed
|
||||
to your terminal.
|
||||
|
@ -116,11 +116,12 @@ standard ``CashState`` in the ``:financial`` Gradle module. The Cash
|
||||
contract uses ``FungibleAsset`` states to model holdings of
|
||||
interchangeable assets and allow the split/merge and summing of
|
||||
states to meet a contractual obligation. We would normally use the
|
||||
``generateSpend`` method on the ``VaultService`` to gather the required
|
||||
``Cash.generateSpend`` method to gather the required
|
||||
amount of cash into a ``TransactionBuilder``, set the outputs and move
|
||||
command. However, to elucidate more clearly example flow code is shown
|
||||
here that will manually carry out the inputs queries by specifying relevant
|
||||
query criteria filters to the ``queryBy`` method of the ``VaultQueryService``.
|
||||
query criteria filters to the ``tryLockFungibleStatesForSpending`` method
|
||||
of the ``VaultService``.
|
||||
|
||||
.. literalinclude:: example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt
|
||||
:language: kotlin
|
||||
|
@ -680,9 +680,9 @@ Finally, we can do redemption.
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, vault: VaultService) {
|
||||
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, services: ServiceHub) {
|
||||
// Add the cash movement using the states in our vault.
|
||||
vault.generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner)
|
||||
Cash.generateSpend(services, tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner)
|
||||
tx.addInputState(paper)
|
||||
tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner.owningKey))
|
||||
}
|
||||
@ -698,7 +698,7 @@ from the issuer of the commercial paper to the current owner. If we don't have e
|
||||
an exception is thrown. Then we add the paper itself as an input, but, not an output (as we wish to remove it
|
||||
from the ledger). Finally, we add a Redeem command that should be signed by the owner of the commercial paper.
|
||||
|
||||
.. warning:: The amount we pass to the ``generateSpend`` function has to be treated first with ``withoutIssuer``.
|
||||
.. warning:: The amount we pass to the ``Cash.generateSpend`` function has to be treated first with ``withoutIssuer``.
|
||||
This reflects the fact that the way we handle issuer constraints is still evolving; the commercial paper
|
||||
contract requires payment in the form of a currency issued by a specific party (e.g. the central bank,
|
||||
or the issuers own bank perhaps). But the vault wants to assemble spend transactions using cash states from
|
||||
|
@ -5,6 +5,7 @@ import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Iterables;
|
||||
import kotlin.Pair;
|
||||
import kotlin.Unit;
|
||||
import net.corda.contracts.asset.Cash;
|
||||
import net.corda.contracts.asset.CashKt;
|
||||
import net.corda.core.contracts.*;
|
||||
import net.corda.core.crypto.SecureHash;
|
||||
@ -12,15 +13,14 @@ import net.corda.core.crypto.testing.NullPublicKey;
|
||||
import net.corda.core.identity.AbstractParty;
|
||||
import net.corda.core.identity.AnonymousParty;
|
||||
import net.corda.core.identity.Party;
|
||||
import net.corda.core.node.services.VaultService;
|
||||
import net.corda.core.node.ServiceHub;
|
||||
import net.corda.core.transactions.LedgerTransaction;
|
||||
import net.corda.core.transactions.TransactionBuilder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Currency;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static net.corda.core.contracts.ContractsDSL.requireSingleCommand;
|
||||
@ -255,8 +255,8 @@ public class JavaCommercialPaper implements Contract {
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
public void generateRedeem(TransactionBuilder tx, StateAndRef<State> paper, VaultService vault) throws InsufficientBalanceException {
|
||||
vault.generateSpend(tx, Structures.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), null);
|
||||
public void generateRedeem(TransactionBuilder tx, StateAndRef<State> paper, ServiceHub services) throws InsufficientBalanceException {
|
||||
Cash.generateSpend(services, tx, Structures.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), Collections.EMPTY_SET);
|
||||
tx.addInputState(paper);
|
||||
tx.addCommand(new Command<>(new Commands.Redeem(), paper.getState().getData().getOwner().getOwningKey()));
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.contracts
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.contracts.asset.sumCashBy
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
@ -9,7 +10,7 @@ import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.schemas.QueryableState
|
||||
@ -144,7 +145,6 @@ class CommercialPaper : Contract {
|
||||
"output values sum to more than the inputs" using (output.faceValue.quantity > 0)
|
||||
"the maturity date is not in the past" using (time < output.maturityDate)
|
||||
// Don't allow an existing CP state to be replaced by this issuance.
|
||||
// TODO: this has a weird/incorrect assertion string because it doesn't quite match the logic in the clause version.
|
||||
// TODO: Consider how to handle the case of mistaken issuances, or other need to patch.
|
||||
"output values sum to more than the inputs" using inputs.isEmpty()
|
||||
}
|
||||
@ -185,9 +185,9 @@ class CommercialPaper : Contract {
|
||||
*/
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
@Suspendable
|
||||
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, vault: VaultService) {
|
||||
fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef<State>, services: ServiceHub) {
|
||||
// Add the cash movement using the states in our vault.
|
||||
vault.generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner)
|
||||
Cash.generateSpend(services, tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner)
|
||||
tx.addInputState(paper)
|
||||
tx.addCommand(Commands.Redeem(), paper.state.data.owner.owningKey)
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package net.corda.contracts.asset
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import co.paralleluniverse.strands.Strand
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.entropyToKeyPair
|
||||
@ -9,16 +11,27 @@ import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.StatesNotAvailableException
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.schemas.QueryableState
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.toHexString
|
||||
import net.corda.core.utilities.toNonEmptySet
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.schemas.CashSchemaV1
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.math.BigInteger
|
||||
import java.security.PublicKey
|
||||
import java.sql.SQLException
|
||||
import java.util.*
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
@ -212,6 +225,163 @@ class Cash : OnLedgerAsset<Currency, Cash.Commands, Cash.State>() {
|
||||
"there is only a single issue command" using (cashCommands.count() == 1)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
// coin selection retry loop counter, sleep (msecs) and lock for selecting states
|
||||
private val MAX_RETRIES = 5
|
||||
private val RETRY_SLEEP = 100
|
||||
private val spendLock: ReentrantLock = ReentrantLock()
|
||||
/**
|
||||
* Generate a transaction that moves an amount of currency to the given pubkey.
|
||||
*
|
||||
* Note: an [Amount] of [Currency] is only fungible for a given Issuer Party within a [FungibleAsset]
|
||||
*
|
||||
* @param services The [ServiceHub] to provide access to the database session.
|
||||
* @param tx A builder, which may contain inputs, outputs and commands already. The relevant components needed
|
||||
* to move the cash will be added on top.
|
||||
* @param amount How much currency to send.
|
||||
* @param to a key of the recipient.
|
||||
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
|
||||
* of given parties. This can be useful if the party you're trying to pay has expectations
|
||||
* about which type of asset claims they are willing to accept.
|
||||
* @return A [Pair] of the same transaction builder passed in as [tx], and the list of keys that need to sign
|
||||
* the resulting transaction for it to be valid.
|
||||
* @throws InsufficientBalanceException when a cash spending transaction fails because
|
||||
* there is insufficient quantity for a given currency (and optionally set of Issuer Parties).
|
||||
*/
|
||||
@JvmStatic
|
||||
@Throws(InsufficientBalanceException::class)
|
||||
@Suspendable
|
||||
fun generateSpend(services: ServiceHub,
|
||||
tx: TransactionBuilder,
|
||||
amount: Amount<Currency>,
|
||||
to: AbstractParty,
|
||||
onlyFromParties: Set<AbstractParty> = emptySet()): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
|
||||
fun deriveState(txState: TransactionState<Cash.State>, amt: Amount<Issued<Currency>>, owner: AbstractParty)
|
||||
= txState.copy(data = txState.data.copy(amount = amt, owner = owner))
|
||||
|
||||
// Retrieve unspent and unlocked cash states that meet our spending criteria.
|
||||
val acceptableCoins = Cash.unconsumedCashStatesForSpending(services, amount, onlyFromParties, tx.notary, tx.lockId)
|
||||
return OnLedgerAsset.generateSpend(tx, amount, to, acceptableCoins,
|
||||
{ state, quantity, owner -> deriveState(state, quantity, owner) },
|
||||
{ Cash().generateMoveCommand() })
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An optimised query to gather Cash states that are available and retry if they are temporarily unavailable.
|
||||
* @param services The service hub to allow access to the database session
|
||||
* @param amount The amount of currency desired (ignoring issues, but specifying the currency)
|
||||
* @param onlyFromIssuerParties If empty the operation ignores the specifics of the issuer,
|
||||
* otherwise the set of eligible states wil be filtered to only include those from these issuers.
|
||||
* @param notary If null the notary source is ignored, if specified then only states marked
|
||||
* with this notary are included.
|
||||
* @param lockId The [FlowLogic.runId.uuid] of the flow, which is used to soft reserve the states.
|
||||
* Also, previous outputs of the flow will be eligible as they are implicitly locked with this id until the flow completes.
|
||||
* @param withIssuerRefs If not empty the specific set of issuer references to match against.
|
||||
* @return The matching states that were found. If sufficient funds were found these will be locked,
|
||||
* otherwise what is available is returned unlocked for informational purposes.
|
||||
*/
|
||||
@JvmStatic
|
||||
@Suspendable
|
||||
fun unconsumedCashStatesForSpending(services: ServiceHub,
|
||||
amount: Amount<Currency>,
|
||||
onlyFromIssuerParties: Set<AbstractParty> = emptySet(),
|
||||
notary: Party? = null,
|
||||
lockId: UUID,
|
||||
withIssuerRefs: Set<OpaqueBytes> = emptySet()): List<StateAndRef<Cash.State>> {
|
||||
|
||||
val issuerKeysStr = onlyFromIssuerParties.fold("") { left, right -> left + "('${right.owningKey.toBase58String()}')," }.dropLast(1)
|
||||
val issuerRefsStr = withIssuerRefs.fold("") { left, right -> left + "('${right.bytes.toHexString()}')," }.dropLast(1)
|
||||
|
||||
val stateAndRefs = mutableListOf<StateAndRef<Cash.State>>()
|
||||
|
||||
// TODO: Need to provide a database provider independent means of performing this function.
|
||||
// We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins:
|
||||
// 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the
|
||||
// running total of such an accumulator
|
||||
// 2) H2 uses session variables to perform this accumulator function:
|
||||
// http://www.h2database.com/html/functions.html#set
|
||||
// 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries)
|
||||
|
||||
for (retryCount in 1..MAX_RETRIES) {
|
||||
|
||||
spendLock.withLock {
|
||||
val statement = services.jdbcSession().createStatement()
|
||||
try {
|
||||
statement.execute("CALL SET(@t, 0);")
|
||||
|
||||
// we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null)
|
||||
// the softLockReserve update will detect whether we try to lock states locked by others
|
||||
val selectJoin = """
|
||||
SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
|
||||
FROM vault_states AS vs, contract_cash_states AS ccs
|
||||
WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
|
||||
AND vs.state_status = 0
|
||||
AND ccs.ccy_code = '${amount.token}' and @t < ${amount.quantity}
|
||||
AND (vs.lock_id = '$lockId' OR vs.lock_id is null)
|
||||
""" +
|
||||
(if (notary != null)
|
||||
" AND vs.notary_key = '${notary.owningKey.toBase58String()}'" else "") +
|
||||
(if (onlyFromIssuerParties.isNotEmpty())
|
||||
" AND ccs.issuer_key IN ($issuerKeysStr)" else "") +
|
||||
(if (withIssuerRefs.isNotEmpty())
|
||||
" AND ccs.issuer_ref IN ($issuerRefsStr)" else "")
|
||||
|
||||
// Retrieve spendable state refs
|
||||
val rs = statement.executeQuery(selectJoin)
|
||||
stateAndRefs.clear()
|
||||
log.debug(selectJoin)
|
||||
var totalPennies = 0L
|
||||
while (rs.next()) {
|
||||
val txHash = SecureHash.parse(rs.getString(1))
|
||||
val index = rs.getInt(2)
|
||||
val stateRef = StateRef(txHash, index)
|
||||
val state = rs.getBytes(3).deserialize<TransactionState<Cash.State>>(context = SerializationDefaults.STORAGE_CONTEXT)
|
||||
val pennies = rs.getLong(4)
|
||||
totalPennies = rs.getLong(5)
|
||||
val rowLockId = rs.getString(6)
|
||||
stateAndRefs.add(StateAndRef(state, stateRef))
|
||||
log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" }
|
||||
}
|
||||
|
||||
if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) {
|
||||
// we should have a minimum number of states to satisfy our selection `amount` criteria
|
||||
log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs")
|
||||
|
||||
// With the current single threaded state machine available states are guaranteed to lock.
|
||||
// TODO However, we will have to revisit these methods in the future multi-threaded.
|
||||
services.vaultService.softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet())
|
||||
return stateAndRefs
|
||||
}
|
||||
log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}")
|
||||
// retry as more states may become available
|
||||
} catch (e: SQLException) {
|
||||
log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId]
|
||||
$e.
|
||||
""")
|
||||
} catch (e: StatesNotAvailableException) { // Should never happen with single threaded state machine
|
||||
stateAndRefs.clear()
|
||||
log.warn(e.message)
|
||||
// retry only if there are locked states that may become available again (or consumed with change)
|
||||
} finally {
|
||||
statement.close()
|
||||
}
|
||||
}
|
||||
|
||||
log.warn("Coin selection failed on attempt $retryCount")
|
||||
// TODO: revisit the back off strategy for contended spending.
|
||||
if (retryCount != MAX_RETRIES) {
|
||||
Strand.sleep(RETRY_SLEEP * retryCount.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
log.warn("Insufficient spendable states identified for $amount")
|
||||
return stateAndRefs
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Small DSL extensions.
|
||||
|
@ -226,8 +226,6 @@ abstract class OnLedgerAsset<T : Any, C : CommandData, S : FungibleAsset<T>> : C
|
||||
*
|
||||
* @param tx transaction builder to add states and commands to.
|
||||
* @param amountIssued the amount to be exited, represented as a quantity of issued currency.
|
||||
* @param changeKey the key to send any change to. This needs to be explicitly stated as the input states are not
|
||||
* necessarily owned by us.
|
||||
* @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is
|
||||
* the responsibility of the caller to check that they do not exit funds held by others.
|
||||
* @return the public keys which must sign the transaction for it to be valid.
|
||||
|
@ -11,8 +11,8 @@ import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.DEFAULT_PAGE_NUM
|
||||
import net.corda.core.node.services.vault.PageSpecification
|
||||
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import java.util.*
|
||||
|
||||
@ -41,7 +41,7 @@ class CashExitFlow(val amount: Amount<Currency>, val issueRef: OpaqueBytes, prog
|
||||
progressTracker.currentStep = GENERATING_TX
|
||||
val builder: TransactionBuilder = TransactionBuilder(notary = null as Party?)
|
||||
val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef)
|
||||
val exitStates = serviceHub.vaultService.unconsumedStatesForSpending<Cash.State>(amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference))
|
||||
val exitStates = Cash.unconsumedCashStatesForSpending(serviceHub, amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference))
|
||||
val signers = try {
|
||||
Cash().generateExit(
|
||||
builder,
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.InsufficientBalanceException
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
@ -26,7 +27,7 @@ open class CashPaymentFlow(
|
||||
val recipient: Party,
|
||||
val anonymous: Boolean,
|
||||
progressTracker: ProgressTracker,
|
||||
val issuerConstraint: Set<Party>? = null) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
|
||||
val issuerConstraint: Set<Party> = emptySet()) : AbstractCashFlow<AbstractCashFlow.Result>(progressTracker) {
|
||||
/** A straightforward constructor that constructs spends using cash states of any issuer. */
|
||||
constructor(amount: Amount<Currency>, recipient: Party) : this(amount, recipient, true, tracker())
|
||||
/** A straightforward constructor that constructs spends using cash states of any issuer. */
|
||||
@ -45,7 +46,7 @@ open class CashPaymentFlow(
|
||||
val builder: TransactionBuilder = TransactionBuilder(null as Party?)
|
||||
// TODO: Have some way of restricting this to states the caller controls
|
||||
val (spendTX, keysForSigning) = try {
|
||||
serviceHub.vaultService.generateSpend(
|
||||
Cash.generateSpend(serviceHub,
|
||||
builder,
|
||||
amount,
|
||||
anonymousRecipient,
|
||||
|
@ -1,8 +1,12 @@
|
||||
package net.corda.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.contracts.asset.sumCashBy
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.OwnableState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.withoutIssuer
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
@ -138,7 +142,8 @@ object TwoPartyTradeFlow {
|
||||
// Put together a proposed transaction that performs the trade, and sign it.
|
||||
progressTracker.currentStep = SIGNING
|
||||
val (ptx, cashSigningPubKeys) = assembleSharedTX(assetForSale, tradeRequest)
|
||||
val partSignedTx = signWithOurKeys(cashSigningPubKeys, ptx)
|
||||
// Now sign the transaction with whatever keys we need to move the cash.
|
||||
val partSignedTx = serviceHub.signInitialTransaction(ptx, cashSigningPubKeys)
|
||||
|
||||
// Send the signed transaction to the seller, who must then sign it themselves and commit
|
||||
// it to the ledger by sending it to the notary.
|
||||
@ -164,17 +169,13 @@ object TwoPartyTradeFlow {
|
||||
}
|
||||
}
|
||||
|
||||
private fun signWithOurKeys(cashSigningPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction {
|
||||
// Now sign the transaction with whatever keys we need to move the cash.
|
||||
return serviceHub.signInitialTransaction(ptx, cashSigningPubKeys)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun assembleSharedTX(assetForSale: StateAndRef<OwnableState>, tradeRequest: SellerTradeInfo): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
val ptx = TransactionBuilder(notary)
|
||||
|
||||
// Add input and output states for the movement of cash, by using the Cash contract to generate the states
|
||||
val (tx, cashSigningPubKeys) = serviceHub.vaultService.generateSpend(ptx, tradeRequest.price, tradeRequest.sellerOwner)
|
||||
val (tx, cashSigningPubKeys) = Cash.generateSpend(serviceHub, ptx, tradeRequest.price, tradeRequest.sellerOwner)
|
||||
|
||||
// Add inputs/outputs/a command for the movement of the asset.
|
||||
tx.addInputState(assetForSale)
|
||||
|
@ -1,21 +1,19 @@
|
||||
package net.corda.contracts
|
||||
|
||||
import net.corda.contracts.asset.*
|
||||
import net.corda.testing.contracts.fillWithSomeTestCash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.fillWithSomeTestCash
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.makeTestDatabaseProperties
|
||||
import net.corda.testing.node.makeTestDatabaseAndMockServices
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
@ -212,40 +210,22 @@ class CommercialPaperTestsGeneric {
|
||||
@Test
|
||||
fun `issue move and then redeem`() {
|
||||
initialiseTestSerialization()
|
||||
val dataSourcePropsAlice = makeTestDataSourceProperties()
|
||||
val databaseAlice = configureDatabase(dataSourcePropsAlice, makeTestDatabaseProperties())
|
||||
val aliceDatabaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(ALICE_KEY))
|
||||
val databaseAlice = aliceDatabaseAndServices.first
|
||||
aliceServices = aliceDatabaseAndServices.second
|
||||
aliceVaultService = aliceServices.vaultService
|
||||
|
||||
databaseAlice.transaction {
|
||||
|
||||
aliceServices = object : MockServices(ALICE_KEY) {
|
||||
override val vaultService: VaultService = makeVaultService(dataSourcePropsAlice)
|
||||
|
||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||
for (stx in txs) {
|
||||
validatedTransactions.addTransaction(stx)
|
||||
}
|
||||
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
|
||||
vaultService.notifyAll(txs.map { it.tx })
|
||||
}
|
||||
}
|
||||
alicesVault = aliceServices.fillWithSomeTestCash(9000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1)
|
||||
aliceVaultService = aliceServices.vaultService
|
||||
}
|
||||
|
||||
val dataSourcePropsBigCorp = makeTestDataSourceProperties()
|
||||
val databaseBigCorp = configureDatabase(dataSourcePropsBigCorp, makeTestDatabaseProperties())
|
||||
val bigCorpDatabaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(BIG_CORP_KEY))
|
||||
val databaseBigCorp = bigCorpDatabaseAndServices.first
|
||||
bigCorpServices = bigCorpDatabaseAndServices.second
|
||||
bigCorpVaultService = bigCorpServices.vaultService
|
||||
|
||||
databaseBigCorp.transaction {
|
||||
|
||||
bigCorpServices = object : MockServices(BIG_CORP_KEY) {
|
||||
override val vaultService: VaultService = makeVaultService(dataSourcePropsBigCorp)
|
||||
|
||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||
for (stx in txs) {
|
||||
validatedTransactions.addTransaction(stx)
|
||||
}
|
||||
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
|
||||
vaultService.notifyAll(txs.map { it.tx })
|
||||
}
|
||||
}
|
||||
bigCorpVault = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1)
|
||||
bigCorpVaultService = bigCorpServices.vaultService
|
||||
}
|
||||
@ -266,7 +246,7 @@ class CommercialPaperTestsGeneric {
|
||||
// Alice pays $9000 to BigCorp to own some of their debt.
|
||||
moveTX = run {
|
||||
val builder = TransactionBuilder(DUMMY_NOTARY)
|
||||
aliceVaultService.generateSpend(builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public))
|
||||
Cash.generateSpend(aliceServices, builder, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public))
|
||||
CommercialPaper().generateMove(builder, issueTx.tx.outRef(0), AnonymousParty(aliceServices.key.public))
|
||||
val ptx = aliceServices.signInitialTransaction(builder)
|
||||
val ptx2 = bigCorpServices.addSignature(ptx)
|
||||
@ -288,7 +268,7 @@ class CommercialPaperTestsGeneric {
|
||||
fun makeRedeemTX(time: Instant): Pair<SignedTransaction, UUID> {
|
||||
val builder = TransactionBuilder(DUMMY_NOTARY)
|
||||
builder.setTimeWindow(time, 30.seconds)
|
||||
CommercialPaper().generateRedeem(builder, moveTX.tx.outRef(1), bigCorpVaultService)
|
||||
CommercialPaper().generateRedeem(builder, moveTX.tx.outRef(1), bigCorpServices)
|
||||
val ptx = aliceServices.signInitialTransaction(builder)
|
||||
val ptx2 = bigCorpServices.addSignature(ptx)
|
||||
val stx = notaryServices.addSignature(ptx2)
|
||||
|
@ -6,26 +6,25 @@ import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.node.services.VaultQueryService
|
||||
import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.node.services.database.HibernateConfiguration
|
||||
import net.corda.node.services.identity.InMemoryIdentityService
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.node.services.vault.HibernateVaultQueryImpl
|
||||
import net.corda.node.services.vault.NodeVaultService
|
||||
import net.corda.node.utilities.CordaPersistence
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.DummyState
|
||||
import net.corda.testing.contracts.fillWithSomeTestCash
|
||||
import net.corda.testing.node.MockKeyManagementService
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.makeTestDatabaseProperties
|
||||
import net.corda.testing.node.makeTestDatabaseAndMockServices
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.security.KeyPair
|
||||
@ -55,25 +54,11 @@ class CashTests : TestDependencyInjectionBase() {
|
||||
@Before
|
||||
fun setUp() {
|
||||
LogHelper.setLevel(NodeVaultService::class)
|
||||
val dataSourceProps = makeTestDataSourceProperties()
|
||||
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties())
|
||||
val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY))
|
||||
database = databaseAndServices.first
|
||||
miniCorpServices = databaseAndServices.second
|
||||
|
||||
database.transaction {
|
||||
val hibernateConfig = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties())
|
||||
miniCorpServices = object : MockServices(MINI_CORP_KEY) {
|
||||
override val keyManagementService: MockKeyManagementService = MockKeyManagementService(identityService, MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY)
|
||||
override val vaultService: VaultService = makeVaultService(dataSourceProps)
|
||||
|
||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||
for (stx in txs) {
|
||||
validatedTransactions.addTransaction(stx)
|
||||
}
|
||||
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
|
||||
vaultService.notifyAll(txs.map { it.tx })
|
||||
}
|
||||
|
||||
override val vaultQueryService: VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
|
||||
}
|
||||
|
||||
miniCorpServices.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
|
||||
issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_IDENTITY_1)
|
||||
miniCorpServices.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1,
|
||||
@ -88,6 +73,11 @@ class CashTests : TestDependencyInjectionBase() {
|
||||
resetTestSerialization()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
database.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun trivial() {
|
||||
transaction {
|
||||
@ -485,7 +475,7 @@ class CashTests : TestDependencyInjectionBase() {
|
||||
fun makeSpend(amount: Amount<Currency>, dest: AbstractParty): WireTransaction {
|
||||
val tx = TransactionBuilder(DUMMY_NOTARY)
|
||||
database.transaction {
|
||||
vault.generateSpend(tx, amount, dest)
|
||||
Cash.generateSpend(miniCorpServices, tx, amount, dest)
|
||||
}
|
||||
return tx.toWireTransaction()
|
||||
}
|
||||
@ -586,7 +576,7 @@ class CashTests : TestDependencyInjectionBase() {
|
||||
database.transaction {
|
||||
|
||||
val tx = TransactionBuilder(DUMMY_NOTARY)
|
||||
vault.generateSpend(tx, 80.DOLLARS, ALICE, setOf(MINI_CORP))
|
||||
Cash.generateSpend(miniCorpServices, tx, 80.DOLLARS, ALICE, setOf(MINI_CORP))
|
||||
|
||||
assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0])
|
||||
}
|
||||
|
@ -0,0 +1,94 @@
|
||||
package net.corda.nodeapi.internal.serialization
|
||||
|
||||
import net.corda.core.serialization.ClassWhitelist
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.nodeapi.internal.serialization.amqp.AmqpHeaderV1_0
|
||||
import net.corda.nodeapi.internal.serialization.amqp.DeserializationInput
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializationOutput
|
||||
import net.corda.nodeapi.internal.serialization.amqp.SerializerFactory
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
private const val AMQP_ENABLED = false
|
||||
|
||||
abstract class AbstractAMQPSerializationScheme : SerializationScheme {
|
||||
private val serializerFactoriesForContexts = ConcurrentHashMap<Pair<ClassWhitelist, ClassLoader>, SerializerFactory>()
|
||||
|
||||
protected abstract fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory
|
||||
protected abstract fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory
|
||||
|
||||
private fun getSerializerFactory(context: SerializationContext): SerializerFactory {
|
||||
return serializerFactoriesForContexts.computeIfAbsent(Pair(context.whitelist, context.deserializationClassLoader)) {
|
||||
when (context.useCase) {
|
||||
SerializationContext.UseCase.Checkpoint ->
|
||||
throw IllegalStateException("AMQP should not be used for checkpoint serialization.")
|
||||
SerializationContext.UseCase.RPCClient ->
|
||||
rpcClientSerializerFactory(context)
|
||||
SerializationContext.UseCase.RPCServer ->
|
||||
rpcServerSerializerFactory(context)
|
||||
else -> SerializerFactory(context.whitelist) // TODO pass class loader also
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T {
|
||||
val serializerFactory = getSerializerFactory(context)
|
||||
return DeserializationInput(serializerFactory).deserialize(byteSequence, clazz)
|
||||
}
|
||||
|
||||
override fun <T : Any> serialize(obj: T, context: SerializationContext): SerializedBytes<T> {
|
||||
val serializerFactory = getSerializerFactory(context)
|
||||
return SerializationOutput(serializerFactory).serialize(obj)
|
||||
}
|
||||
|
||||
protected fun canDeserializeVersion(byteSequence: ByteSequence): Boolean = AMQP_ENABLED && byteSequence == AmqpHeaderV1_0
|
||||
}
|
||||
|
||||
// TODO: This will eventually cover server RPC as well and move to node module, but for now this is not implemented
|
||||
class AMQPServerSerializationScheme : AbstractAMQPSerializationScheme() {
|
||||
override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean {
|
||||
return (canDeserializeVersion(byteSequence) &&
|
||||
(target == SerializationContext.UseCase.P2P || target == SerializationContext.UseCase.Storage))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TODO: This will eventually cover client RPC as well and move to client module, but for now this is not implemented
|
||||
class AMQPClientSerializationScheme : AbstractAMQPSerializationScheme() {
|
||||
override fun rpcClientSerializerFactory(context: SerializationContext): SerializerFactory {
|
||||
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
|
||||
}
|
||||
|
||||
override fun rpcServerSerializerFactory(context: SerializationContext): SerializerFactory {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun canDeserializeVersion(byteSequence: ByteSequence, target: SerializationContext.UseCase): Boolean {
|
||||
return (canDeserializeVersion(byteSequence) &&
|
||||
(target == SerializationContext.UseCase.P2P || target == SerializationContext.UseCase.Storage))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
val AMQP_P2P_CONTEXT = SerializationContextImpl(AmqpHeaderV1_0,
|
||||
SerializationDefaults.javaClass.classLoader,
|
||||
GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()),
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.P2P)
|
||||
val AMQP_STORAGE_CONTEXT = SerializationContextImpl(AmqpHeaderV1_0,
|
||||
SerializationDefaults.javaClass.classLoader,
|
||||
AllButBlacklisted,
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.Storage)
|
@ -54,6 +54,8 @@ data class SerializationContextImpl(override val preferedSerializationVersion: B
|
||||
}
|
||||
}
|
||||
|
||||
private const val HEADER_SIZE: Int = 8
|
||||
|
||||
open class SerializationFactoryImpl : SerializationFactory {
|
||||
private val creator: List<StackTraceElement> = Exception().stackTrace.asList()
|
||||
|
||||
@ -63,8 +65,8 @@ open class SerializationFactoryImpl : SerializationFactory {
|
||||
private val schemes: ConcurrentHashMap<Pair<ByteSequence, SerializationContext.UseCase>, SerializationScheme> = ConcurrentHashMap()
|
||||
|
||||
private fun schemeFor(byteSequence: ByteSequence, target: SerializationContext.UseCase): SerializationScheme {
|
||||
// truncate sequence to 8 bytes
|
||||
return schemes.computeIfAbsent(byteSequence.take(8).copy() to target) {
|
||||
// truncate sequence to 8 bytes, and make sure it's a copy to avoid holding onto large ByteArrays
|
||||
return schemes.computeIfAbsent(byteSequence.take(HEADER_SIZE).copy() to target) {
|
||||
for (scheme in registeredSchemes) {
|
||||
if (scheme.canDeserializeVersion(it.first, it.second)) {
|
||||
return@computeIfAbsent scheme
|
||||
@ -162,11 +164,12 @@ abstract class AbstractKryoSerializationScheme : SerializationScheme {
|
||||
|
||||
override fun <T : Any> deserialize(byteSequence: ByteSequence, clazz: Class<T>, context: SerializationContext): T {
|
||||
val pool = getPool(context)
|
||||
Input(byteSequence.bytes, byteSequence.offset, byteSequence.size).use { input ->
|
||||
val header = OpaqueBytes(input.readBytes(8))
|
||||
if (header != KryoHeaderV0_1) {
|
||||
throw KryoException("Serialized bytes header does not match expected format.")
|
||||
}
|
||||
val headerSize = KryoHeaderV0_1.size
|
||||
val header = byteSequence.take(headerSize)
|
||||
if (header != KryoHeaderV0_1) {
|
||||
throw KryoException("Serialized bytes header does not match expected format.")
|
||||
}
|
||||
Input(byteSequence.bytes, byteSequence.offset + headerSize, byteSequence.size - headerSize).use { input ->
|
||||
return pool.run { kryo ->
|
||||
withContext(kryo, context) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
@ -2,6 +2,7 @@ package net.corda.nodeapi.internal.serialization.amqp
|
||||
|
||||
import net.corda.core.internal.getStackTraceAsString
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import org.apache.qpid.proton.amqp.Binary
|
||||
import org.apache.qpid.proton.amqp.DescribedType
|
||||
import org.apache.qpid.proton.amqp.UnsignedByte
|
||||
@ -26,17 +27,6 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S
|
||||
internal companion object {
|
||||
val BYTES_NEEDED_TO_PEEK: Int = 23
|
||||
|
||||
private fun subArraysEqual(a: ByteArray, aOffset: Int, length: Int, b: ByteArray, bOffset: Int): Boolean {
|
||||
if (aOffset + length > a.size || bOffset + length > b.size) throw IndexOutOfBoundsException()
|
||||
var bytesRemaining = length
|
||||
var aPos = aOffset
|
||||
var bPos = bOffset
|
||||
while (bytesRemaining-- > 0) {
|
||||
if (a[aPos++] != b[bPos++]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun peekSize(bytes: ByteArray): Int {
|
||||
// There's an 8 byte header, and then a 0 byte plus descriptor followed by constructor
|
||||
val eighth = bytes[8].toInt()
|
||||
@ -69,15 +59,16 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S
|
||||
|
||||
|
||||
@Throws(NotSerializableException::class)
|
||||
private fun <T : Any> getEnvelope(bytes: SerializedBytes<T>): Envelope {
|
||||
private fun getEnvelope(bytes: ByteSequence): Envelope {
|
||||
// Check that the lead bytes match expected header
|
||||
if (!subArraysEqual(bytes.bytes, 0, 8, AmqpHeaderV1_0.bytes, 0)) {
|
||||
val headerSize = AmqpHeaderV1_0.size
|
||||
if (bytes.take(headerSize) != AmqpHeaderV1_0) {
|
||||
throw NotSerializableException("Serialization header does not match.")
|
||||
}
|
||||
|
||||
val data = Data.Factory.create()
|
||||
val size = data.decode(ByteBuffer.wrap(bytes.bytes, 8, bytes.size - 8))
|
||||
if (size.toInt() != bytes.size - 8) {
|
||||
val size = data.decode(ByteBuffer.wrap(bytes.bytes, bytes.offset + headerSize, bytes.size - headerSize))
|
||||
if (size.toInt() != bytes.size - headerSize) {
|
||||
throw NotSerializableException("Unexpected size of data")
|
||||
}
|
||||
|
||||
@ -103,7 +94,7 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S
|
||||
* be deserialized and a schema describing the types of the objects.
|
||||
*/
|
||||
@Throws(NotSerializableException::class)
|
||||
fun <T : Any> deserialize(bytes: SerializedBytes<T>, clazz: Class<T>): T {
|
||||
fun <T : Any> deserialize(bytes: ByteSequence, clazz: Class<T>): T {
|
||||
return des {
|
||||
val envelope = getEnvelope(bytes)
|
||||
clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz))
|
||||
|
@ -11,20 +11,14 @@ import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.sequence
|
||||
import net.corda.node.serialization.KryoServerSerializationScheme
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.ALICE_PUBKEY
|
||||
import net.corda.testing.BOB
|
||||
import net.corda.testing.BOB_PUBKEY
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.time.Instant
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
@ -150,26 +144,6 @@ class KryoTests {
|
||||
assertEquals(-1, readRubbishStream.read())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize - deserialize X509CertififcateHolder`() {
|
||||
val expected: X509CertificateHolder = X509Utilities.createSelfSignedCACertificate(ALICE.name, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
|
||||
val serialized = expected.serialize(factory, context).bytes
|
||||
val actual: X509CertificateHolder = serialized.deserialize(factory, context)
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize - deserialize X509CertPath`() {
|
||||
val certFactory = CertificateFactory.getInstance("X509")
|
||||
val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val rootCACert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootCAKey)
|
||||
val certificate = X509Utilities.createCertificate(CertificateType.TLS, rootCACert, rootCAKey, BOB.name, BOB_PUBKEY)
|
||||
val expected = certFactory.generateCertPath(listOf(certificate.cert, rootCACert.cert))
|
||||
val serialized = expected.serialize(factory, context).bytes
|
||||
val actual: CertPath = serialized.deserialize(factory, context)
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
private data class Person(val name: String, val birthday: Instant?)
|
||||
|
||||
|
@ -87,7 +87,6 @@ processSmokeTestResources {
|
||||
// build/reports/project/dependencies/index.html for green highlighted parts of the tree.
|
||||
|
||||
dependencies {
|
||||
compile project(':finance')
|
||||
compile project(':node-schemas')
|
||||
compile project(':node-api')
|
||||
compile project(':client:rpc')
|
||||
@ -150,6 +149,7 @@ dependencies {
|
||||
testCompile "com.pholser:junit-quickcheck-core:$quickcheck_version"
|
||||
testCompile project(':test-utils')
|
||||
testCompile project(':client:jfx')
|
||||
testCompile project(':finance')
|
||||
|
||||
// sample test schemas
|
||||
testCompile project(path: ':finance', configuration: 'testArtifacts')
|
||||
|
@ -1,10 +1,8 @@
|
||||
package net.corda.services.messaging
|
||||
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.internal.copyTo
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.crypto.CertificateType
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.X509Utilities
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.node.utilities.*
|
||||
import net.corda.nodeapi.ArtemisMessagingComponent.Companion.NODE_USER
|
||||
|
@ -479,7 +479,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
private fun makeVaultObservers() {
|
||||
VaultSoftLockManager(services.vaultService, smm)
|
||||
ScheduledActivityObserver(services)
|
||||
HibernateObserver(services.vaultService.rawUpdates, HibernateConfiguration(services.schemaService, configuration.database ?: Properties()))
|
||||
HibernateObserver(services.vaultService.rawUpdates, HibernateConfiguration(services.schemaService, configuration.database ?: Properties(), services.identityService))
|
||||
}
|
||||
|
||||
private fun makeInfo(): NodeInfo {
|
||||
@ -766,7 +766,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
override val networkMapCache by lazy { InMemoryNetworkMapCache(this) }
|
||||
override val vaultService by lazy { NodeVaultService(this, configuration.dataSourceProperties, configuration.database) }
|
||||
override val vaultQueryService by lazy {
|
||||
HibernateVaultQueryImpl(HibernateConfiguration(schemaService, configuration.database ?: Properties()), vaultService.updatesPublisher)
|
||||
HibernateVaultQueryImpl(HibernateConfiguration(schemaService, configuration.database ?: Properties(), identityService), vaultService.updatesPublisher)
|
||||
}
|
||||
// Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because
|
||||
// the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with
|
||||
|
@ -332,6 +332,7 @@ open class Node(override val configuration: FullNodeConfiguration,
|
||||
private fun initialiseSerialization() {
|
||||
SerializationDefaults.SERIALIZATION_FACTORY = SerializationFactoryImpl().apply {
|
||||
registerScheme(KryoServerSerializationScheme())
|
||||
registerScheme(AMQPServerSerializationScheme())
|
||||
}
|
||||
SerializationDefaults.P2P_CONTEXT = KRYO_P2P_CONTEXT
|
||||
SerializationDefaults.RPC_SERVER_CONTEXT = KRYO_RPC_SERVER_CONTEXT
|
||||
|
@ -4,9 +4,10 @@ import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigParseOptions
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SignatureScheme
|
||||
import net.corda.core.internal.copyTo
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.core.utilities.loggerFor
|
||||
|
@ -1,7 +1,9 @@
|
||||
package net.corda.node.services.database
|
||||
|
||||
import net.corda.core.internal.castIfPossible
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.converters.AbstractPartyToX500NameAsStringConverter
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.api.SchemaService
|
||||
import net.corda.node.utilities.DatabaseTransactionManager
|
||||
@ -18,7 +20,7 @@ import java.sql.Connection
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class HibernateConfiguration(val schemaService: SchemaService, val databaseProperties: Properties) {
|
||||
class HibernateConfiguration(val schemaService: SchemaService, val databaseProperties: Properties, val identitySvc: IdentityService) {
|
||||
companion object {
|
||||
val logger = loggerFor<HibernateConfiguration>()
|
||||
}
|
||||
@ -58,6 +60,7 @@ class HibernateConfiguration(val schemaService: SchemaService, val databasePrope
|
||||
val config = Configuration(metadataSources).setProperty("hibernate.connection.provider_class", HibernateConfiguration.NodeDatabaseConnectionProvider::class.java.name)
|
||||
.setProperty("hibernate.hbm2ddl.auto", if (databaseProperties.getProperty("initDatabase","true") == "true") "update" else "validate")
|
||||
.setProperty("hibernate.format_sql", "true")
|
||||
|
||||
schemas.forEach { schema ->
|
||||
// TODO: require mechanism to set schemaOptions (databaseSchema, tablePrefix) which are not global to session
|
||||
schema.mappedTypes.forEach { config.addAnnotatedClass(it) }
|
||||
@ -76,6 +79,9 @@ class HibernateConfiguration(val schemaService: SchemaService, val databasePrope
|
||||
return Identifier.toIdentifier(tablePrefix + default.text, default.isQuoted)
|
||||
}
|
||||
})
|
||||
// register custom converters
|
||||
applyAttributeConverter(AbstractPartyToX500NameAsStringConverter(identitySvc))
|
||||
|
||||
build()
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,14 @@
|
||||
package net.corda.node.services.keys
|
||||
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.ContentSignerBuilder
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.cert
|
||||
import net.corda.core.identity.AnonymousPartyAndPath
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.node.utilities.CertificateType
|
||||
import net.corda.node.utilities.X509Utilities
|
||||
import org.bouncycastle.operator.ContentSigner
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
@ -31,7 +35,7 @@ fun freshCertificate(identityService: IdentityService,
|
||||
revocationEnabled: Boolean = false): AnonymousPartyAndPath {
|
||||
val issuerCertificate = issuer.certificate
|
||||
val window = X509Utilities.getCertificateValidityWindow(Duration.ZERO, 3650.days, issuerCertificate)
|
||||
val ourCertificate = Crypto.createCertificate(CertificateType.IDENTITY, issuerCertificate.subject, issuerSigner, issuer.name, subjectPublicKey, window)
|
||||
val ourCertificate = X509Utilities.createCertificate(CertificateType.IDENTITY, issuerCertificate.subject, issuerSigner, issuer.name, subjectPublicKey, window)
|
||||
val certFactory = CertificateFactory.getInstance("X509")
|
||||
val ourCertPath = certFactory.generateCertPath(listOf(ourCertificate.cert) + issuer.certPath.certificates)
|
||||
val anonymisedIdentity = AnonymousPartyAndPath(subjectPublicKey, ourCertPath)
|
||||
|
@ -2,13 +2,13 @@ package net.corda.node.services.messaging
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import io.netty.handler.ssl.SslHandler
|
||||
import net.corda.core.*
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_TLS
|
||||
import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
import net.corda.core.crypto.AddressFormatException
|
||||
import net.corda.core.crypto.newSecureRandom
|
||||
import net.corda.core.crypto.parsePublicKeyBase58
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.noneOrSingle
|
||||
import net.corda.core.node.NodeInfo
|
||||
@ -22,6 +22,9 @@ import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE
|
||||
import net.corda.node.services.messaging.NodeLoginModule.Companion.PEER_ROLE
|
||||
import net.corda.node.services.messaging.NodeLoginModule.Companion.RPC_ROLE
|
||||
import net.corda.node.services.messaging.NodeLoginModule.Companion.VERIFIER_ROLE
|
||||
import net.corda.node.utilities.X509Utilities
|
||||
import net.corda.node.utilities.X509Utilities.CORDA_CLIENT_TLS
|
||||
import net.corda.node.utilities.X509Utilities.CORDA_ROOT_CA
|
||||
import net.corda.node.utilities.getX509Certificate
|
||||
import net.corda.node.utilities.loadKeyStore
|
||||
import net.corda.nodeapi.*
|
||||
|
@ -8,13 +8,13 @@ import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.VaultQueryException
|
||||
import net.corda.core.node.services.vault.*
|
||||
import net.corda.core.node.services.vault.QueryCriteria.CommonQueryCriteria
|
||||
import net.corda.core.schemas.CommonSchemaV1
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.schemas.PersistentStateRef
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.toHexString
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.toHexString
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.core.schemas.CommonSchemaV1
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.util.*
|
||||
import javax.persistence.Tuple
|
||||
|
@ -8,28 +8,32 @@ import io.requery.kotlin.eq
|
||||
import io.requery.kotlin.notNull
|
||||
import io.requery.query.RowExpression
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.contracts.asset.OnLedgerAsset
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.containsAny
|
||||
import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.internal.tee
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.StatesNotAvailableException
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.node.services.vault.IQueryCriteriaParser
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.node.services.vault.Sort
|
||||
import net.corda.core.node.services.vault.SortAttribute
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
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.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.core.utilities.NonEmptySet
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.toNonEmptySet
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.node.services.database.RequeryConfiguration
|
||||
import net.corda.node.services.database.parserTransactionIsolationLevel
|
||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||
@ -42,10 +46,8 @@ import net.corda.node.utilities.wrapWithDatabaseTransaction
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.security.PublicKey
|
||||
import java.sql.SQLException
|
||||
import java.util.*
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
import javax.persistence.criteria.Predicate
|
||||
|
||||
/**
|
||||
* Currently, the node vault service is a very simple RDBMS backed implementation. It will change significantly when
|
||||
@ -79,6 +81,7 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
|
||||
// For use during publishing only.
|
||||
val updatesPublisher: rx.Observer<Vault.Update<ContractState>> get() = _updatesPublisher.bufferUntilDatabaseCommit().tee(_rawUpdatesPublisher)
|
||||
}
|
||||
|
||||
private val mutex = ThreadBox(InnerState())
|
||||
|
||||
private fun recordUpdate(update: Vault.Update<ContractState>): Vault.Update<ContractState> {
|
||||
@ -334,123 +337,110 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
|
||||
}
|
||||
}
|
||||
|
||||
// coin selection retry loop counter, sleep (msecs) and lock for selecting states
|
||||
val MAX_RETRIES = 5
|
||||
val RETRY_SLEEP = 100
|
||||
val spendLock: ReentrantLock = ReentrantLock()
|
||||
// TODO We shouldn't need to rewrite the query if we could modify the defaults.
|
||||
private class QueryEditor<out T : ContractState>(val services: ServiceHub,
|
||||
val lockId: UUID,
|
||||
val contractType: Class<out T>) : IQueryCriteriaParser {
|
||||
var alreadyHasVaultQuery: Boolean = false
|
||||
var modifiedCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(contractStateTypes = setOf(contractType),
|
||||
softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)),
|
||||
status = Vault.StateStatus.UNCONSUMED)
|
||||
|
||||
@Suspendable
|
||||
override fun <T : ContractState> unconsumedStatesForSpending(amount: Amount<Currency>, onlyFromIssuerParties: Set<AbstractParty>?, notary: Party?, lockId: UUID, withIssuerRefs: Set<OpaqueBytes>?): List<StateAndRef<T>> {
|
||||
|
||||
val issuerKeysStr = onlyFromIssuerParties?.fold("") { left, right -> left + "('${right.owningKey.toBase58String()}')," }?.dropLast(1)
|
||||
val issuerRefsStr = withIssuerRefs?.fold("") { left, right -> left + "('${right.bytes.toHexString()}')," }?.dropLast(1)
|
||||
|
||||
val stateAndRefs = mutableListOf<StateAndRef<T>>()
|
||||
|
||||
// TODO: Need to provide a database provider independent means of performing this function.
|
||||
// We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins:
|
||||
// 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the
|
||||
// running total of such an accumulator
|
||||
// 2) H2 uses session variables to perform this accumulator function:
|
||||
// http://www.h2database.com/html/functions.html#set
|
||||
// 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries)
|
||||
|
||||
for (retryCount in 1..MAX_RETRIES) {
|
||||
|
||||
spendLock.withLock {
|
||||
val statement = configuration.jdbcSession().createStatement()
|
||||
try {
|
||||
statement.execute("CALL SET(@t, 0);")
|
||||
|
||||
// we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null)
|
||||
// the softLockReserve update will detect whether we try to lock states locked by others
|
||||
val selectJoin = """
|
||||
SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
|
||||
FROM vault_states AS vs, contract_cash_states AS ccs
|
||||
WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
|
||||
AND vs.state_status = 0
|
||||
AND ccs.ccy_code = '${amount.token}' and @t < ${amount.quantity}
|
||||
AND (vs.lock_id = '$lockId' OR vs.lock_id is null)
|
||||
""" +
|
||||
(if (notary != null)
|
||||
" AND vs.notary_key = '${notary.owningKey.toBase58String()}'" else "") +
|
||||
(if (issuerKeysStr != null)
|
||||
" AND ccs.issuer_key IN ($issuerKeysStr)" else "") +
|
||||
(if (issuerRefsStr != null)
|
||||
" AND ccs.issuer_ref IN ($issuerRefsStr)" else "")
|
||||
|
||||
// Retrieve spendable state refs
|
||||
val rs = statement.executeQuery(selectJoin)
|
||||
stateAndRefs.clear()
|
||||
log.debug(selectJoin)
|
||||
var totalPennies = 0L
|
||||
while (rs.next()) {
|
||||
val txHash = SecureHash.parse(rs.getString(1))
|
||||
val index = rs.getInt(2)
|
||||
val stateRef = StateRef(txHash, index)
|
||||
val state = rs.getBytes(3).deserialize<TransactionState<T>>(context = STORAGE_CONTEXT)
|
||||
val pennies = rs.getLong(4)
|
||||
totalPennies = rs.getLong(5)
|
||||
val rowLockId = rs.getString(6)
|
||||
stateAndRefs.add(StateAndRef(state, stateRef))
|
||||
log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" }
|
||||
}
|
||||
|
||||
if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) {
|
||||
// we should have a minimum number of states to satisfy our selection `amount` criteria
|
||||
log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs")
|
||||
|
||||
// update database
|
||||
softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet())
|
||||
return stateAndRefs
|
||||
}
|
||||
log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}")
|
||||
// retry as more states may become available
|
||||
} catch (e: SQLException) {
|
||||
log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId]
|
||||
$e.
|
||||
""")
|
||||
} catch (e: StatesNotAvailableException) {
|
||||
stateAndRefs.clear()
|
||||
log.warn(e.message)
|
||||
// retry only if there are locked states that may become available again (or consumed with change)
|
||||
} finally {
|
||||
statement.close()
|
||||
}
|
||||
}
|
||||
|
||||
log.warn("Coin selection failed on attempt $retryCount")
|
||||
// TODO: revisit the back off strategy for contended spending.
|
||||
if (retryCount != MAX_RETRIES) {
|
||||
FlowStateMachineImpl.sleep(RETRY_SLEEP * retryCount.toLong())
|
||||
}
|
||||
override fun parseCriteria(criteria: QueryCriteria.CommonQueryCriteria): Collection<Predicate> {
|
||||
modifiedCriteria = criteria
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
log.warn("Insufficient spendable states identified for $amount")
|
||||
return stateAndRefs
|
||||
override fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection<Predicate> {
|
||||
modifiedCriteria = criteria
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection<Predicate> {
|
||||
modifiedCriteria = criteria
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun <L : PersistentState> parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria<L>): Collection<Predicate> {
|
||||
modifiedCriteria = criteria
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria): Collection<Predicate> {
|
||||
modifiedCriteria = criteria.copy(contractStateTypes = setOf(contractType),
|
||||
softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)),
|
||||
status = Vault.StateStatus.UNCONSUMED)
|
||||
alreadyHasVaultQuery = true
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun parseOr(left: QueryCriteria, right: QueryCriteria): Collection<Predicate> {
|
||||
parse(left)
|
||||
val modifiedLeft = modifiedCriteria
|
||||
parse(right)
|
||||
val modifiedRight = modifiedCriteria
|
||||
modifiedCriteria = modifiedLeft.or(modifiedRight)
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection<Predicate> {
|
||||
parse(left)
|
||||
val modifiedLeft = modifiedCriteria
|
||||
parse(right)
|
||||
val modifiedRight = modifiedCriteria
|
||||
modifiedCriteria = modifiedLeft.and(modifiedRight)
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override fun parse(criteria: QueryCriteria, sorting: Sort?): Collection<Predicate> {
|
||||
val basicQuery = modifiedCriteria
|
||||
criteria.visit(this)
|
||||
modifiedCriteria = if (alreadyHasVaultQuery) modifiedCriteria else criteria.and(basicQuery)
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
fun queryForEligibleStates(criteria: QueryCriteria): Vault.Page<T> {
|
||||
val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF)
|
||||
val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))
|
||||
parse(criteria, sorter)
|
||||
|
||||
return services.vaultQueryService.queryBy(contractType, modifiedCriteria, sorter)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a transaction that moves an amount of currency to the given pubkey.
|
||||
*
|
||||
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
|
||||
* of given parties. This can be useful if the party you're trying to pay has expectations
|
||||
* about which type of asset claims they are willing to accept.
|
||||
*/
|
||||
|
||||
@Suspendable
|
||||
override fun generateSpend(tx: TransactionBuilder,
|
||||
amount: Amount<Currency>,
|
||||
to: AbstractParty,
|
||||
onlyFromParties: Set<AbstractParty>?): Pair<TransactionBuilder, List<PublicKey>> {
|
||||
// Retrieve unspent and unlocked cash states that meet our spending criteria.
|
||||
val acceptableCoins = unconsumedStatesForSpending<Cash.State>(amount, onlyFromParties, tx.notary, tx.lockId)
|
||||
return OnLedgerAsset.generateSpend(tx, amount, to, acceptableCoins,
|
||||
{ state, quantity, owner -> deriveState(state, quantity, owner) },
|
||||
{ Cash().generateMoveCommand() })
|
||||
}
|
||||
@Throws(StatesNotAvailableException::class)
|
||||
override fun <T : FungibleAsset<U>, U : Any> tryLockFungibleStatesForSpending(lockId: UUID,
|
||||
eligibleStatesQuery: QueryCriteria,
|
||||
amount: Amount<U>,
|
||||
contractType: Class<out T>): List<StateAndRef<T>> {
|
||||
if (amount.quantity == 0L) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun deriveState(txState: TransactionState<Cash.State>, amount: Amount<Issued<Currency>>, owner: AbstractParty)
|
||||
= txState.copy(data = txState.data.copy(amount = amount, owner = owner))
|
||||
// TODO This helper code re-writes the query to alter the defaults on things such as soft locks
|
||||
// and then runs the query. Ideally we would not need to do this.
|
||||
val results = QueryEditor(services, lockId, contractType).queryForEligibleStates(eligibleStatesQuery)
|
||||
|
||||
var claimedAmount = 0L
|
||||
val claimedStates = mutableListOf<StateAndRef<T>>()
|
||||
for (state in results.states) {
|
||||
val issuedAssetToken = state.state.data.amount.token
|
||||
if (issuedAssetToken.product == amount.token) {
|
||||
claimedStates += state
|
||||
claimedAmount += state.state.data.amount.quantity
|
||||
if (claimedAmount > amount.quantity) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (claimedStates.isEmpty() || claimedAmount < amount.quantity) {
|
||||
return emptyList()
|
||||
}
|
||||
softLockReserve(lockId, claimedStates.map { it.ref }.toNonEmptySet())
|
||||
return claimedStates
|
||||
}
|
||||
|
||||
// TODO : Persists this in DB.
|
||||
private val authorisedUpgrade = mutableMapOf<StateRef, Class<out UpgradedContract<*, *>>>()
|
||||
|
@ -105,8 +105,8 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio
|
||||
var participants: Set<CommonSchemaV1.Party>,
|
||||
|
||||
/** [OwnableState] attributes */
|
||||
@OneToOne(cascade = arrayOf(CascadeType.ALL))
|
||||
var owner: CommonSchemaV1.Party,
|
||||
@Column(name = "owner_id")
|
||||
var owner: AbstractParty,
|
||||
|
||||
/** [FungibleAsset] attributes
|
||||
*
|
||||
@ -126,7 +126,7 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio
|
||||
var issuerRef: ByteArray
|
||||
) : PersistentState() {
|
||||
constructor(_owner: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: OpaqueBytes, _participants: List<AbstractParty>) :
|
||||
this(owner = CommonSchemaV1.Party(_owner),
|
||||
this(owner = _owner,
|
||||
quantity = _quantity,
|
||||
issuerParty = CommonSchemaV1.Party(_issuerParty),
|
||||
issuerRef = _issuerRef.bytes,
|
||||
|
@ -1,25 +1,33 @@
|
||||
package net.corda.core.crypto
|
||||
package net.corda.node.utilities
|
||||
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.utilities.millis
|
||||
import org.bouncycastle.asn1.ASN1Encodable
|
||||
import org.bouncycastle.asn1.ASN1EncodableVector
|
||||
import org.bouncycastle.asn1.ASN1Sequence
|
||||
import org.bouncycastle.asn1.DERSequence
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x500.X500NameBuilder
|
||||
import org.bouncycastle.asn1.x500.style.BCStyle
|
||||
import org.bouncycastle.asn1.x509.KeyPurposeId
|
||||
import org.bouncycastle.asn1.x509.KeyUsage
|
||||
import org.bouncycastle.asn1.x509.NameConstraints
|
||||
import org.bouncycastle.asn1.x509.*
|
||||
import org.bouncycastle.asn1.x509.Extension
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder
|
||||
import org.bouncycastle.cert.bc.BcX509ExtensionUtils
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
|
||||
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.util.io.pem.PemReader
|
||||
import java.io.FileReader
|
||||
import java.io.FileWriter
|
||||
import java.io.InputStream
|
||||
import java.math.BigInteger
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.*
|
||||
import java.security.cert.Certificate
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
@ -69,44 +77,13 @@ object X509Utilities {
|
||||
return Pair(notBefore, notAfter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a bogus X509 for dev purposes. Use [getX509Name] for something more real.
|
||||
*/
|
||||
@Deprecated("Full legal names should be specified in all configurations")
|
||||
fun getDevX509Name(commonName: String): X500Name {
|
||||
val nameBuilder = X500NameBuilder(BCStyle.INSTANCE)
|
||||
nameBuilder.addRDN(BCStyle.CN, commonName)
|
||||
nameBuilder.addRDN(BCStyle.O, "R3")
|
||||
nameBuilder.addRDN(BCStyle.OU, "corda")
|
||||
nameBuilder.addRDN(BCStyle.L, "London")
|
||||
nameBuilder.addRDN(BCStyle.C, "GB")
|
||||
return nameBuilder.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a distinguished name from the provided values.
|
||||
*
|
||||
* @see [CoreTestUtils.getTestX509Name] for generating distinguished names for test cases.
|
||||
*/
|
||||
@JvmOverloads
|
||||
@JvmStatic
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Create a de novo root self-signed X509 v3 CA cert.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun createSelfSignedCACertificate(subject: X500Name, keyPair: KeyPair, validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW): X509CertificateHolder {
|
||||
val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second)
|
||||
return Crypto.createCertificate(CertificateType.ROOT_CA, subject, keyPair, subject, keyPair.public, window)
|
||||
return createCertificate(CertificateType.ROOT_CA, subject, keyPair, subject, keyPair.public, window)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -126,7 +103,7 @@ object X509Utilities {
|
||||
validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW,
|
||||
nameConstraints: NameConstraints? = null): X509CertificateHolder {
|
||||
val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, issuerCertificate)
|
||||
return Crypto.createCertificate(certificateType, issuerCertificate.subject, issuerKeyPair, subject, subjectPublicKey, window, nameConstraints)
|
||||
return createCertificate(certificateType, issuerCertificate.subject, issuerKeyPair, subject, subjectPublicKey, window, nameConstraints)
|
||||
}
|
||||
|
||||
fun validateCertificateChain(trustedRoot: X509CertificateHolder, vararg certificates: Certificate) {
|
||||
@ -168,54 +145,93 @@ object X509Utilities {
|
||||
}
|
||||
}
|
||||
|
||||
fun createCertificateSigningRequest(subject: X500Name, keyPair: KeyPair, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME) = Crypto.createCertificateSigningRequest(subject, keyPair, signatureScheme)
|
||||
}
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
/**
|
||||
* 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 }
|
||||
val serial = BigInteger.valueOf(random63BitValue())
|
||||
val keyPurposes = DERSequence(ASN1EncodableVector().apply { certificateType.purposes.forEach { add(it) } })
|
||||
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(subjectPublicKey.encoded))
|
||||
|
||||
/**
|
||||
* 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 }
|
||||
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)
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
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()))
|
||||
}
|
||||
}
|
||||
require(matched) { "Input X.500 name must include a common name (CN) attribute: ${this}" }
|
||||
return builder.build()
|
||||
|
||||
/**
|
||||
* 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 = Crypto.findSignatureScheme(issuerKeyPair.private)
|
||||
val provider = Crypto.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, Crypto.providerMap[signatureScheme.providerName])
|
||||
return JcaPKCS10CertificationRequestBuilder(subject, keyPair.public).build(signer)
|
||||
}
|
||||
|
||||
fun createCertificateSigningRequest(subject: X500Name, keyPair: KeyPair) = createCertificateSigningRequest(subject, keyPair, DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
class CertificateStream(val input: InputStream) {
|
||||
private val certificateFactory = CertificateFactory.getInstance("X.509")
|
||||
@ -223,8 +239,6 @@ class CertificateStream(val input: InputStream) {
|
||||
fun nextCertificate(): X509Certificate = certificateFactory.generateCertificate(input) as X509Certificate
|
||||
}
|
||||
|
||||
data class CertificateAndKeyPair(val certificate: X509CertificateHolder, val keyPair: KeyPair)
|
||||
|
||||
enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurposeId, val isCA: Boolean) {
|
||||
ROOT_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true),
|
||||
INTERMEDIATE_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true),
|
@ -1,7 +1,7 @@
|
||||
package net.corda.node.utilities.registration
|
||||
|
||||
import com.google.common.net.MediaType
|
||||
import net.corda.core.crypto.CertificateStream
|
||||
import net.corda.node.utilities.CertificateStream
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import java.io.IOException
|
||||
|
@ -1,17 +1,15 @@
|
||||
package net.corda.node.utilities.registration
|
||||
|
||||
import net.corda.core.crypto.CertificateType
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.X509Utilities
|
||||
import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_CA
|
||||
import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_TLS
|
||||
import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA
|
||||
import net.corda.core.crypto.cert
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.core.utilities.validateX500Name
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.utilities.*
|
||||
import net.corda.node.utilities.X509Utilities.CORDA_CLIENT_CA
|
||||
import net.corda.node.utilities.X509Utilities.CORDA_CLIENT_TLS
|
||||
import net.corda.node.utilities.X509Utilities.CORDA_ROOT_CA
|
||||
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
|
||||
import org.bouncycastle.util.io.pem.PemObject
|
||||
import java.io.StringWriter
|
||||
|
@ -1,42 +1,52 @@
|
||||
package net.corda.node.services.vault;
|
||||
|
||||
import com.google.common.collect.*;
|
||||
import net.corda.contracts.*;
|
||||
import net.corda.contracts.asset.*;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import kotlin.Pair;
|
||||
import net.corda.contracts.DealState;
|
||||
import net.corda.contracts.asset.Cash;
|
||||
import net.corda.core.contracts.*;
|
||||
import net.corda.core.crypto.*;
|
||||
import net.corda.core.identity.*;
|
||||
import net.corda.core.messaging.*;
|
||||
import net.corda.core.node.services.*;
|
||||
import net.corda.core.crypto.EncodingUtils;
|
||||
import net.corda.core.identity.AbstractParty;
|
||||
import net.corda.core.messaging.DataFeed;
|
||||
import net.corda.core.node.services.Vault;
|
||||
import net.corda.core.node.services.VaultQueryException;
|
||||
import net.corda.core.node.services.VaultQueryService;
|
||||
import net.corda.core.node.services.VaultService;
|
||||
import net.corda.core.node.services.vault.*;
|
||||
import net.corda.core.node.services.vault.QueryCriteria.*;
|
||||
import net.corda.core.schemas.*;
|
||||
import net.corda.core.transactions.*;
|
||||
import net.corda.core.utilities.*;
|
||||
import net.corda.node.services.database.*;
|
||||
import net.corda.node.services.schema.*;
|
||||
import net.corda.node.utilities.*;
|
||||
import net.corda.schemas.*;
|
||||
import net.corda.testing.*;
|
||||
import net.corda.testing.contracts.*;
|
||||
import net.corda.testing.node.*;
|
||||
import net.corda.testing.schemas.*;
|
||||
import org.jetbrains.annotations.*;
|
||||
import org.junit.*;
|
||||
import net.corda.core.node.services.vault.QueryCriteria.LinearStateQueryCriteria;
|
||||
import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria;
|
||||
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria;
|
||||
import net.corda.core.utilities.OpaqueBytes;
|
||||
import net.corda.node.utilities.CordaPersistence;
|
||||
import net.corda.node.services.identity.*;
|
||||
import net.corda.schemas.CashSchemaV1;
|
||||
import net.corda.testing.TestConstants;
|
||||
import net.corda.testing.TestDependencyInjectionBase;
|
||||
import net.corda.testing.contracts.DummyLinearContract;
|
||||
import net.corda.testing.contracts.VaultFiller;
|
||||
import net.corda.testing.node.MockServices;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import rx.Observable;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.reflect.*;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.security.KeyPair;
|
||||
import java.util.*;
|
||||
import java.util.stream.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import static net.corda.contracts.asset.CashKt.*;
|
||||
import static net.corda.core.node.services.vault.QueryCriteriaUtils.*;
|
||||
import static net.corda.core.utilities.ByteArrays.*;
|
||||
import static net.corda.node.utilities.CordaPersistenceKt.*;
|
||||
import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER;
|
||||
import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER_KEY;
|
||||
import static net.corda.core.node.services.vault.QueryCriteriaUtils.DEFAULT_PAGE_NUM;
|
||||
import static net.corda.core.node.services.vault.QueryCriteriaUtils.MAX_PAGE_SIZE;
|
||||
import static net.corda.core.utilities.ByteArrays.toHexString;
|
||||
import static net.corda.testing.CoreTestUtils.*;
|
||||
import static net.corda.testing.node.MockServicesKt.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static net.corda.testing.node.MockServicesKt.makeTestDatabaseAndMockServices;
|
||||
import static net.corda.testing.TestConstants.*;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
||||
|
||||
@ -47,38 +57,14 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
Properties dataSourceProps = makeTestDataSourceProperties(SecureHash.randomSHA256().toString());
|
||||
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties());
|
||||
database.transaction(statement -> {
|
||||
Set<MappedSchema> customSchemas = new HashSet<>(Collections.singletonList(DummyLinearStateSchemaV1.INSTANCE));
|
||||
HibernateConfiguration hibernateConfig = new HibernateConfiguration(new NodeSchemaService(customSchemas), makeTestDatabaseProperties());
|
||||
services = new MockServices(getMEGA_CORP_KEY()) {
|
||||
@NotNull
|
||||
@Override
|
||||
public VaultService getVaultService() {
|
||||
if (vaultSvc != null) return vaultSvc;
|
||||
return makeVaultService(dataSourceProps, hibernateConfig);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public VaultQueryService getVaultQueryService() {
|
||||
return new HibernateVaultQueryImpl(hibernateConfig, vaultSvc.getUpdatesPublisher());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordTransactions(@NotNull Iterable<SignedTransaction> txs) {
|
||||
for (SignedTransaction stx : txs) {
|
||||
getValidatedTransactions().addTransaction(stx);
|
||||
}
|
||||
Stream<WireTransaction> wtxn = StreamSupport.stream(txs.spliterator(), false).map(SignedTransaction::getTx);
|
||||
vaultSvc.notifyAll(wtxn.collect(Collectors.toList()));
|
||||
}
|
||||
};
|
||||
vaultSvc = services.getVaultService();
|
||||
vaultQuerySvc = services.getVaultQueryService();
|
||||
return services;
|
||||
});
|
||||
ArrayList<KeyPair> keys = new ArrayList<>();
|
||||
keys.add(getMEGA_CORP_KEY());
|
||||
InMemoryIdentityService identityService = new InMemoryIdentityService(getMOCK_IDENTITIES(), Collections.emptyMap(), getDUMMY_CA().getCertificate());
|
||||
Pair<CordaPersistence, MockServices> databaseAndServices = makeTestDatabaseAndMockServices(Collections.EMPTY_SET, keys);
|
||||
database = databaseAndServices.getFirst();
|
||||
services = databaseAndServices.getSecond();
|
||||
vaultSvc = services.getVaultService();
|
||||
vaultQuerySvc = services.getVaultQueryService();
|
||||
}
|
||||
|
||||
@After
|
||||
|
@ -148,8 +148,8 @@ class TwoPartyTradeFlowTests {
|
||||
bobNode.disableDBCloseOnStop()
|
||||
|
||||
val cashStates = bobNode.database.transaction {
|
||||
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, notaryNode.info.notaryIdentity, 3, 3)
|
||||
}
|
||||
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, notaryNode.info.notaryIdentity, 3, 3)
|
||||
}
|
||||
|
||||
val alicesFakePaper = aliceNode.database.transaction {
|
||||
fillUpForSeller(false, cpIssuer, aliceNode.info.legalIdentity,
|
||||
@ -168,7 +168,7 @@ class TwoPartyTradeFlowTests {
|
||||
}
|
||||
|
||||
val (bobStateMachine, aliceResult) = runBuyerAndSeller(notaryNode, aliceNode, bobNode,
|
||||
"alice's paper".outputStateAndRef())
|
||||
"alice's paper".outputStateAndRef())
|
||||
|
||||
assertEquals(aliceResult.getOrThrow(), bobStateMachine.getOrThrow().resultFuture.getOrThrow())
|
||||
|
||||
@ -533,11 +533,11 @@ class TwoPartyTradeFlowTests {
|
||||
override fun call(): SignedTransaction {
|
||||
send(buyer, Pair(notary.notaryIdentity, price))
|
||||
return subFlow(Seller(
|
||||
buyer,
|
||||
notary,
|
||||
assetToSell,
|
||||
price,
|
||||
me.party))
|
||||
buyer,
|
||||
notary,
|
||||
assetToSell,
|
||||
price,
|
||||
me.party))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,17 +3,21 @@ package net.corda.node.services.database
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
|
||||
import net.corda.contracts.asset.DummyFungibleContract
|
||||
import net.corda.contracts.asset.sumCash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.VaultQueryService
|
||||
import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.schemas.CommonSchemaV1
|
||||
import net.corda.core.schemas.PersistentStateRef
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.node.services.identity.InMemoryIdentityService
|
||||
import net.corda.node.services.schema.HibernateObserver
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.node.services.vault.HibernateVaultQueryImpl
|
||||
import net.corda.node.services.vault.NodeVaultService
|
||||
import net.corda.node.services.vault.VaultSchemaV1
|
||||
import net.corda.node.utilities.CordaPersistence
|
||||
@ -38,6 +42,7 @@ import org.junit.After
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.persistence.EntityManager
|
||||
@ -67,7 +72,8 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() {
|
||||
database = configureDatabase(dataSourceProps, defaultDatabaseProperties)
|
||||
val customSchemas = setOf(VaultSchemaV1, CashSchemaV1, SampleCashSchemaV2, SampleCashSchemaV3)
|
||||
database.transaction {
|
||||
hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), makeTestDatabaseProperties())
|
||||
val identityService = InMemoryIdentityService(MOCK_IDENTITIES, trustRoot = DUMMY_CA.certificate)
|
||||
hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), makeTestDatabaseProperties(), identityService)
|
||||
services = object : MockServices(BOB_KEY) {
|
||||
override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)
|
||||
|
||||
@ -126,7 +132,8 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() {
|
||||
|
||||
// execute query
|
||||
val queryResults = entityManager.createQuery(criteriaQuery).resultList
|
||||
assertThat(queryResults.size).isEqualTo(6)
|
||||
val coins = queryResults.map { it.contractState.deserialize<TransactionState<Cash.State>>().data }.sumCash()
|
||||
assertThat(coins.toDecimal() >= BigDecimal("50.00"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.node.services.events
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.containsAny
|
||||
import net.corda.core.flows.*
|
||||
@ -15,10 +16,11 @@ import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
|
||||
import net.corda.core.node.services.vault.Sort
|
||||
import net.corda.core.node.services.vault.SortAttribute
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.statemachine.StateMachineManager
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.After
|
||||
@ -30,6 +32,10 @@ import java.time.Instant
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ScheduledFlowTests {
|
||||
companion object {
|
||||
val PAGE_SIZE = 20
|
||||
val SORTING = Sort(listOf(Sort.SortColumn(SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID), Sort.Direction.DESC)))
|
||||
}
|
||||
lateinit var mockNet: MockNetwork
|
||||
lateinit var notaryNode: MockNetwork.MockNode
|
||||
lateinit var nodeA: MockNetwork.MockNode
|
||||
@ -133,33 +139,50 @@ class ScheduledFlowTests {
|
||||
@Test
|
||||
fun `run a whole batch of scheduled flows`() {
|
||||
val N = 100
|
||||
val futures = mutableListOf<CordaFuture<*>>()
|
||||
for (i in 0..N - 1) {
|
||||
nodeA.services.startFlow(InsertInitialStateFlow(nodeB.info.legalIdentity))
|
||||
nodeB.services.startFlow(InsertInitialStateFlow(nodeA.info.legalIdentity))
|
||||
futures.add(nodeA.services.startFlow(InsertInitialStateFlow(nodeB.info.legalIdentity)).resultFuture)
|
||||
futures.add(nodeB.services.startFlow(InsertInitialStateFlow(nodeA.info.legalIdentity)).resultFuture)
|
||||
}
|
||||
mockNet.waitQuiescent()
|
||||
|
||||
val statesFromA = nodeA.database.transaction {
|
||||
// Check all of the flows completed successfully
|
||||
futures.forEach { it.getOrThrow() }
|
||||
|
||||
// Convert the states into maps to make error reporting easier
|
||||
val statesFromA: List<StateAndRef<ScheduledState>> = nodeA.database.transaction {
|
||||
queryStatesWithPaging(nodeA.services.vaultQueryService)
|
||||
}
|
||||
val statesFromB = nodeB.database.transaction {
|
||||
val statesFromB: List<StateAndRef<ScheduledState>> = nodeB.database.transaction {
|
||||
queryStatesWithPaging(nodeB.services.vaultQueryService)
|
||||
}
|
||||
assertEquals(2 * N, statesFromA.count(), "Expect all states to be present")
|
||||
statesFromA.forEach { ref ->
|
||||
if (ref !in statesFromB) {
|
||||
throw IllegalStateException("State $ref is only present on node A.")
|
||||
}
|
||||
}
|
||||
statesFromB.forEach { ref ->
|
||||
if (ref !in statesFromA) {
|
||||
throw IllegalStateException("State $ref is only present on node B.")
|
||||
}
|
||||
}
|
||||
assertEquals(statesFromA, statesFromB, "Expect identical data on both nodes")
|
||||
assertTrue("Expect all states have run the scheduled task", statesFromB.all { it.state.data.processed })
|
||||
}
|
||||
|
||||
// Demonstrate Vault Query paging and sorting
|
||||
val PAGE_SIZE = 20
|
||||
val sorting = Sort(listOf(Sort.SortColumn(SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID), Sort.Direction.DESC)))
|
||||
|
||||
/**
|
||||
* Query all states from the Vault, fetching results as a series of pages with ordered states in order to perform
|
||||
* integration testing of that functionality.
|
||||
*
|
||||
* @return states ordered by the transaction ID.
|
||||
*/
|
||||
private fun queryStatesWithPaging(vaultQueryService: VaultQueryService): List<StateAndRef<ScheduledState>> {
|
||||
var pageNumber = DEFAULT_PAGE_NUM
|
||||
val states = mutableListOf<StateAndRef<ScheduledState>>()
|
||||
do {
|
||||
val pageSpec = PageSpecification(pageSize = PAGE_SIZE, pageNumber = pageNumber)
|
||||
val results = vaultQueryService.queryBy<ScheduledState>(VaultQueryCriteria(), pageSpec, sorting)
|
||||
val results = vaultQueryService.queryBy<ScheduledState>(VaultQueryCriteria(), pageSpec, SORTING)
|
||||
states.addAll(results.states)
|
||||
pageNumber++
|
||||
} while ((pageSpec.pageSize * (pageNumber)) <= results.totalStatesAvailable)
|
||||
|
@ -1,12 +1,17 @@
|
||||
package net.corda.node.services.network
|
||||
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.AnonymousPartyAndPath
|
||||
import net.corda.core.crypto.CertificateAndKeyPair
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.cert
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.AnonymousPartyAndPath
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.node.services.identity.InMemoryIdentityService
|
||||
import net.corda.node.utilities.CertificateType
|
||||
import net.corda.node.utilities.X509Utilities
|
||||
import net.corda.testing.*
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.junit.Test
|
||||
|
@ -10,9 +10,12 @@ import net.corda.core.schemas.QueryableState
|
||||
import net.corda.testing.LogHelper
|
||||
import net.corda.node.services.api.SchemaService
|
||||
import net.corda.node.services.database.HibernateConfiguration
|
||||
import net.corda.node.services.identity.InMemoryIdentityService
|
||||
import net.corda.node.utilities.CordaPersistence
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.testing.DUMMY_CA
|
||||
import net.corda.testing.MEGA_CORP
|
||||
import net.corda.testing.MOCK_IDENTITIES
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.makeTestDatabaseProperties
|
||||
import org.hibernate.annotations.Cascade
|
||||
@ -102,7 +105,8 @@ class HibernateObserverTests {
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
val observer = HibernateObserver(rawUpdatesPublisher, HibernateConfiguration(schemaService, makeTestDatabaseProperties()))
|
||||
val identityService = InMemoryIdentityService(MOCK_IDENTITIES, trustRoot = DUMMY_CA.certificate)
|
||||
val observer = HibernateObserver(rawUpdatesPublisher, HibernateConfiguration(schemaService, makeTestDatabaseProperties(), identityService))
|
||||
database.transaction {
|
||||
rawUpdatesPublisher.onNext(Vault.Update(emptySet(), setOf(StateAndRef(TransactionState(TestState(), MEGA_CORP), StateRef(SecureHash.sha256("dummy"), 0)))))
|
||||
val parentRowCountResult = TransactionManager.current().connection.prepareStatement("select count(*) from Parents").executeQuery()
|
||||
|
@ -1,18 +1,22 @@
|
||||
package net.corda.node.services.vault
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
|
||||
import net.corda.contracts.asset.sumCash
|
||||
import net.corda.contracts.getCashBalance
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.StatesNotAvailableException
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.node.services.VaultQueryService
|
||||
import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.node.services.vault.QueryCriteria.*
|
||||
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
@ -20,20 +24,20 @@ import net.corda.core.utilities.NonEmptySet
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.toNonEmptySet
|
||||
import net.corda.node.services.database.HibernateConfiguration
|
||||
import net.corda.node.services.identity.InMemoryIdentityService
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.node.utilities.CordaPersistence
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.fillWithSomeTestCash
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.makeTestDatabaseProperties
|
||||
import net.corda.testing.node.makeTestDatabaseAndMockServices
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import rx.observers.TestSubscriber
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.Executors
|
||||
@ -50,23 +54,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
@Before
|
||||
fun setUp() {
|
||||
LogHelper.setLevel(NodeVaultService::class)
|
||||
val dataSourceProps = makeTestDataSourceProperties()
|
||||
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties())
|
||||
database.transaction {
|
||||
val hibernateConfig = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties())
|
||||
services = object : MockServices() {
|
||||
override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)
|
||||
|
||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||
for (stx in txs) {
|
||||
validatedTransactions.addTransaction(stx)
|
||||
}
|
||||
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
|
||||
vaultService.notifyAll(txs.map { it.tx })
|
||||
}
|
||||
override val vaultQueryService : VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
|
||||
}
|
||||
}
|
||||
val databaseAndServices = makeTestDatabaseAndMockServices()
|
||||
database = databaseAndServices.first
|
||||
services = databaseAndServices.second
|
||||
}
|
||||
|
||||
@After
|
||||
@ -75,6 +65,25 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
LogHelper.reset(NodeVaultService::class)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun VaultService.unconsumedCashStatesForSpending(amount: Amount<Currency>,
|
||||
onlyFromIssuerParties: Set<AbstractParty>? = null,
|
||||
notary: Party? = null,
|
||||
lockId: UUID = UUID.randomUUID(),
|
||||
withIssuerRefs: Set<OpaqueBytes>? = null): List<StateAndRef<Cash.State>> {
|
||||
|
||||
val notaryName = if (notary != null) listOf(notary.name) else null
|
||||
var baseCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notaryName = notaryName)
|
||||
if (onlyFromIssuerParties != null || withIssuerRefs != null) {
|
||||
baseCriteria = baseCriteria.and(QueryCriteria.FungibleAssetQueryCriteria(
|
||||
issuerPartyName = onlyFromIssuerParties?.toList(),
|
||||
issuerRef = withIssuerRefs?.toList()))
|
||||
}
|
||||
|
||||
return tryLockFungibleStatesForSpending(lockId, baseCriteria, amount, Cash.State::class.java)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `states not local to instance`() {
|
||||
database.transaction {
|
||||
@ -308,7 +317,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
|
||||
assertThat(unconsumedStates).hasSize(1)
|
||||
|
||||
val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending<Cash.State>(100.DOLLARS, lockId = UUID.randomUUID())
|
||||
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(100.DOLLARS)
|
||||
spendableStatesUSD.forEach(::println)
|
||||
assertThat(spendableStatesUSD).hasSize(1)
|
||||
assertThat(spendableStatesUSD[0].state.data.amount.quantity).isEqualTo(100L * 100)
|
||||
@ -324,12 +333,13 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (DUMMY_CASH_ISSUER))
|
||||
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1)), issuerKey = BOC_KEY)
|
||||
|
||||
val spendableStatesUSD = vaultSvc.unconsumedStatesForSpending<Cash.State>(200.DOLLARS, lockId = UUID.randomUUID(),
|
||||
onlyFromIssuerParties = setOf(DUMMY_CASH_ISSUER.party, BOC)).toList()
|
||||
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(200.DOLLARS,
|
||||
onlyFromIssuerParties = setOf(DUMMY_CASH_ISSUER.party, BOC))
|
||||
spendableStatesUSD.forEach(::println)
|
||||
assertThat(spendableStatesUSD).hasSize(2)
|
||||
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer).isEqualTo(DUMMY_CASH_ISSUER)
|
||||
assertThat(spendableStatesUSD[1].state.data.amount.token.issuer).isEqualTo(BOC.ref(1))
|
||||
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer).isIn(DUMMY_CASH_ISSUER, BOC.ref(1))
|
||||
assertThat(spendableStatesUSD[1].state.data.amount.token.issuer).isIn(DUMMY_CASH_ISSUER, BOC.ref(1))
|
||||
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer).isNotEqualTo(spendableStatesUSD[1].state.data.amount.token.issuer)
|
||||
}
|
||||
}
|
||||
|
||||
@ -345,12 +355,13 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
|
||||
assertThat(unconsumedStates).hasSize(4)
|
||||
|
||||
val spendableStatesUSD = vaultSvc.unconsumedStatesForSpending<Cash.State>(200.DOLLARS, lockId = UUID.randomUUID(),
|
||||
onlyFromIssuerParties = setOf(BOC), withIssuerRefs = setOf(OpaqueBytes.of(1), OpaqueBytes.of(2))).toList()
|
||||
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(200.DOLLARS,
|
||||
onlyFromIssuerParties = setOf(BOC), withIssuerRefs = setOf(OpaqueBytes.of(1), OpaqueBytes.of(2)))
|
||||
assertThat(spendableStatesUSD).hasSize(2)
|
||||
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.party).isEqualTo(BOC)
|
||||
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.reference).isEqualTo(BOC.ref(1).reference)
|
||||
assertThat(spendableStatesUSD[1].state.data.amount.token.issuer.reference).isEqualTo(BOC.ref(2).reference)
|
||||
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.reference).isIn(BOC.ref(1).reference, BOC.ref(2).reference)
|
||||
assertThat(spendableStatesUSD[1].state.data.amount.token.issuer.reference).isIn(BOC.ref(1).reference, BOC.ref(2).reference)
|
||||
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.reference).isNotEqualTo(spendableStatesUSD[1].state.data.amount.token.issuer.reference)
|
||||
}
|
||||
}
|
||||
|
||||
@ -363,9 +374,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
|
||||
assertThat(unconsumedStates).hasSize(1)
|
||||
|
||||
val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending<Cash.State>(110.DOLLARS, lockId = UUID.randomUUID())
|
||||
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(110.DOLLARS)
|
||||
spendableStatesUSD.forEach(::println)
|
||||
assertThat(spendableStatesUSD).hasSize(1)
|
||||
assertThat(spendableStatesUSD).hasSize(0)
|
||||
val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY))
|
||||
assertThat(vaultQuery.queryBy<Cash.State>(criteriaLocked).states).hasSize(0)
|
||||
}
|
||||
@ -380,7 +391,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
|
||||
assertThat(unconsumedStates).hasSize(2)
|
||||
|
||||
val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending<Cash.State>(1.DOLLARS, lockId = UUID.randomUUID())
|
||||
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(1.DOLLARS)
|
||||
spendableStatesUSD.forEach(::println)
|
||||
assertThat(spendableStatesUSD).hasSize(1)
|
||||
assertThat(spendableStatesUSD[0].state.data.amount.quantity).isGreaterThanOrEqualTo(100L)
|
||||
@ -397,16 +408,30 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 10, 10, Random(0L))
|
||||
services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 10, 10, Random(0L))
|
||||
|
||||
var unlockedStates = 30
|
||||
val allStates = vaultQuery.queryBy<Cash.State>().states
|
||||
assertThat(allStates).hasSize(30)
|
||||
assertThat(allStates).hasSize(unlockedStates)
|
||||
|
||||
var lockedCount = 0
|
||||
for (i in 1..5) {
|
||||
val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending<Cash.State>(20.DOLLARS, lockId = UUID.randomUUID())
|
||||
val lockId = UUID.randomUUID()
|
||||
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(20.DOLLARS, lockId = lockId)
|
||||
spendableStatesUSD.forEach(::println)
|
||||
assertThat(spendableStatesUSD.size <= unlockedStates)
|
||||
unlockedStates -= spendableStatesUSD.size
|
||||
val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.SPECIFIED, listOf(lockId)))
|
||||
val lockedStates = vaultQuery.queryBy<Cash.State>(criteriaLocked).states
|
||||
if (spendableStatesUSD.isNotEmpty()) {
|
||||
assertEquals(spendableStatesUSD.size, lockedStates.size)
|
||||
val lockedTotal = lockedStates.map { it.state.data }.sumCash()
|
||||
val foundAmount = spendableStatesUSD.map { it.state.data }.sumCash()
|
||||
assertThat(foundAmount.toDecimal() >= BigDecimal("20.00"))
|
||||
assertThat(lockedTotal == foundAmount)
|
||||
lockedCount += lockedStates.size
|
||||
}
|
||||
}
|
||||
// note only 3 spend attempts succeed with a total of 8 states
|
||||
val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY))
|
||||
assertThat(vaultQuery.queryBy<Cash.State>(criteriaLocked).states).hasSize(8)
|
||||
assertThat(vaultQuery.queryBy<Cash.State>(criteriaLocked).states).hasSize(lockedCount)
|
||||
}
|
||||
}
|
||||
|
||||
@ -484,7 +509,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
|
||||
database.transaction {
|
||||
val moveTx = TransactionBuilder(services.myInfo.legalIdentity).apply {
|
||||
service.generateSpend(this, Amount(1000, GBP), thirdPartyIdentity)
|
||||
Cash.generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity)
|
||||
}.toWireTransaction()
|
||||
service.notify(moveTx)
|
||||
}
|
||||
@ -530,7 +555,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
|
||||
// Move cash
|
||||
val moveTx = database.transaction {
|
||||
TransactionBuilder(newNotary).apply {
|
||||
service.generateSpend(this, Amount(1000, GBP), thirdPartyIdentity)
|
||||
Cash.generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity)
|
||||
}.toWireTransaction()
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,14 @@
|
||||
package net.corda.node.services.vault
|
||||
|
||||
import net.corda.contracts.*
|
||||
import net.corda.contracts.CommercialPaper
|
||||
import net.corda.contracts.Commodity
|
||||
import net.corda.contracts.DealState
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.entropyToKeyPair
|
||||
import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.*
|
||||
import net.corda.core.node.services.vault.*
|
||||
@ -17,7 +19,9 @@ import net.corda.core.utilities.NonEmptySet
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.toHexString
|
||||
import net.corda.node.services.database.HibernateConfiguration
|
||||
import net.corda.node.services.identity.InMemoryIdentityService
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.node.utilities.CordaPersistence
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.schemas.CashSchemaV1
|
||||
@ -27,7 +31,7 @@ import net.corda.schemas.SampleCashSchemaV3
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.*
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.makeTestDatabaseAndMockServices
|
||||
import net.corda.testing.node.makeTestDatabaseProperties
|
||||
import net.corda.testing.schemas.DummyLinearStateSchemaV1
|
||||
import org.assertj.core.api.Assertions
|
||||
@ -54,24 +58,9 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
val dataSourceProps = makeTestDataSourceProperties()
|
||||
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties())
|
||||
database.transaction {
|
||||
val customSchemas = setOf(CommercialPaperSchemaV1, DummyLinearStateSchemaV1)
|
||||
val hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), makeTestDatabaseProperties())
|
||||
services = object : MockServices(MEGA_CORP_KEY) {
|
||||
override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)
|
||||
|
||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||
for (stx in txs) {
|
||||
validatedTransactions.addTransaction(stx)
|
||||
}
|
||||
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
|
||||
vaultService.notifyAll(txs.map { it.tx })
|
||||
}
|
||||
override val vaultQueryService : VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
|
||||
}
|
||||
}
|
||||
val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MEGA_CORP_KEY))
|
||||
database = databaseAndServices.first
|
||||
services = databaseAndServices.second
|
||||
}
|
||||
|
||||
@After
|
||||
@ -97,7 +86,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
||||
_database.transaction {
|
||||
|
||||
// create new states
|
||||
services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 10, 10, Random(0L))
|
||||
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
|
||||
val linearStatesXYZ = services.fillWithSomeTestLinearStates(1, "XYZ")
|
||||
val linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL")
|
||||
services.fillWithSomeTestLinearStates(3, "ABC")
|
||||
@ -239,15 +228,15 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
||||
fun `unconsumed cash states sorted by state ref`() {
|
||||
database.transaction {
|
||||
|
||||
var stateRefs : MutableList<StateRef> = mutableListOf()
|
||||
val stateRefs: MutableList<StateRef> = mutableListOf()
|
||||
|
||||
val issuedStates = services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
|
||||
val issuedStateRefs = issuedStates.states.map { it.ref }.toList()
|
||||
stateRefs.addAll(issuedStateRefs)
|
||||
|
||||
val spentStates = services.consumeCash(25.DOLLARS)
|
||||
var consumedStateRefs = spentStates.consumed.map { it.ref }.toList()
|
||||
var producedStateRefs = spentStates.produced.map { it.ref }.toList()
|
||||
val consumedStateRefs = spentStates.consumed.map { it.ref }.toList()
|
||||
val producedStateRefs = spentStates.produced.map { it.ref }.toList()
|
||||
stateRefs.addAll(consumedStateRefs.plus(producedStateRefs))
|
||||
|
||||
val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF)
|
||||
@ -271,8 +260,9 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
||||
fun `unconsumed cash states sorted by state ref txnId and index`() {
|
||||
database.transaction {
|
||||
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
|
||||
services.consumeCash(10.DOLLARS)
|
||||
services.consumeCash(10.DOLLARS)
|
||||
val consumed = mutableSetOf<SecureHash>()
|
||||
services.consumeCash(10.DOLLARS).consumed.forEach { consumed += it.ref.txhash }
|
||||
services.consumeCash(10.DOLLARS).consumed.forEach { consumed += it.ref.txhash }
|
||||
|
||||
val sortAttributeTxnId = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID)
|
||||
val sortAttributeIndex = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_INDEX)
|
||||
@ -283,13 +273,11 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
||||
|
||||
results.statesMetadata.forEach {
|
||||
println(" ${it.ref}")
|
||||
assertThat(it.status).isEqualTo(Vault.StateStatus.UNCONSUMED)
|
||||
}
|
||||
|
||||
// explicit sort order asc by txnId and then index:
|
||||
// order by
|
||||
// vaultschem1_.transaction_id asc,
|
||||
// vaultschem1_.output_index asc
|
||||
assertThat(results.states).hasSize(9) // -2 CONSUMED + 1 NEW UNCONSUMED (change)
|
||||
val sorted = results.states.sortedBy { it.ref.toString() }
|
||||
assertThat(results.states).isEqualTo(sorted)
|
||||
assertThat(results.states).allSatisfy { !consumed.contains(it.ref.txhash) }
|
||||
}
|
||||
}
|
||||
|
||||
@ -411,7 +399,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
||||
}
|
||||
}
|
||||
|
||||
val CASH_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) }
|
||||
val CASH_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(21)) }
|
||||
val CASH_NOTARY: Party get() = Party(X500Name("CN=Cash Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), CASH_NOTARY_KEY.public)
|
||||
|
||||
@Test
|
||||
@ -870,7 +858,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
||||
fun `aggregate functions count by contract type and state status`() {
|
||||
database.transaction {
|
||||
// create new states
|
||||
services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 10, 10, Random(0L))
|
||||
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
|
||||
val linearStatesXYZ = services.fillWithSomeTestLinearStates(1, "XYZ")
|
||||
val linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL")
|
||||
services.fillWithSomeTestLinearStates(3, "ABC")
|
||||
@ -896,14 +884,14 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
||||
services.consumeLinearStates(linearStatesXYZ.states.toList())
|
||||
services.consumeLinearStates(linearStatesJKL.states.toList())
|
||||
services.consumeDeals(dealStates.states.filter { it.state.data.ref == "456" })
|
||||
services.consumeCash(50.DOLLARS)
|
||||
val cashUpdates = services.consumeCash(50.DOLLARS)
|
||||
|
||||
// UNCONSUMED states (default)
|
||||
|
||||
// count fungible assets
|
||||
val countCriteriaUnconsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.UNCONSUMED)
|
||||
val fungibleStateCountUnconsumed = vaultQuerySvc.queryBy<FungibleAsset<*>>(countCriteriaUnconsumed).otherResults.single() as Long
|
||||
assertThat(fungibleStateCountUnconsumed).isEqualTo(5L)
|
||||
assertThat(fungibleStateCountUnconsumed.toInt()).isEqualTo(10 - cashUpdates.consumed.size + cashUpdates.produced.size)
|
||||
|
||||
// count linear states
|
||||
val linearStateCountUnconsumed = vaultQuerySvc.queryBy<LinearState>(countCriteriaUnconsumed).otherResults.single() as Long
|
||||
@ -918,7 +906,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
||||
// count fungible assets
|
||||
val countCriteriaConsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.CONSUMED)
|
||||
val fungibleStateCountConsumed = vaultQuerySvc.queryBy<FungibleAsset<*>>(countCriteriaConsumed).otherResults.single() as Long
|
||||
assertThat(fungibleStateCountConsumed).isEqualTo(6L)
|
||||
assertThat(fungibleStateCountConsumed.toInt()).isEqualTo(cashUpdates.consumed.size)
|
||||
|
||||
// count linear states
|
||||
val linearStateCountConsumed = vaultQuerySvc.queryBy<LinearState>(countCriteriaConsumed).otherResults.single() as Long
|
||||
@ -962,7 +950,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
|
||||
fun `states consumed after time`() {
|
||||
database.transaction {
|
||||
|
||||
services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 3, 3, Random(0L))
|
||||
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L))
|
||||
services.fillWithSomeTestLinearStates(10)
|
||||
services.fillWithSomeTestDeals(listOf("123", "456", "789"))
|
||||
|
||||
|
@ -11,17 +11,15 @@ import net.corda.core.node.services.VaultService
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.node.services.database.HibernateConfiguration
|
||||
import net.corda.node.services.identity.InMemoryIdentityService
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.node.utilities.CordaPersistence
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.*
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.makeTestDatabaseProperties
|
||||
import net.corda.testing.node.makeTestDatabaseAndMockServices
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.After
|
||||
@ -44,23 +42,9 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
@Before
|
||||
fun setUp() {
|
||||
LogHelper.setLevel(VaultWithCashTest::class)
|
||||
val dataSourceProps = makeTestDataSourceProperties()
|
||||
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties())
|
||||
database.transaction {
|
||||
val hibernateConfig = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties())
|
||||
services = object : MockServices() {
|
||||
override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)
|
||||
|
||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||
for (stx in txs) {
|
||||
validatedTransactions.addTransaction(stx)
|
||||
}
|
||||
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
|
||||
vaultService.notifyAll(txs.map { it.tx })
|
||||
}
|
||||
override val vaultQueryService : VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
|
||||
}
|
||||
}
|
||||
val databaseAndServices = makeTestDatabaseAndMockServices()
|
||||
database = databaseAndServices.first
|
||||
services = databaseAndServices.second
|
||||
}
|
||||
|
||||
@After
|
||||
@ -103,7 +87,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
|
||||
// A tx that spends our money.
|
||||
val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY)
|
||||
vault.generateSpend(spendTXBuilder, 80.DOLLARS, BOB)
|
||||
Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB)
|
||||
val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey)
|
||||
val spendTX = notaryServices.addSignature(spendPTX)
|
||||
|
||||
@ -151,7 +135,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
database.transaction {
|
||||
try {
|
||||
val txn1Builder = TransactionBuilder(DUMMY_NOTARY)
|
||||
vault.generateSpend(txn1Builder, 60.DOLLARS, BOB)
|
||||
Cash.generateSpend(services, txn1Builder, 60.DOLLARS, BOB)
|
||||
val ptxn1 = notaryServices.signInitialTransaction(txn1Builder)
|
||||
val txn1 = services.addSignature(ptxn1, freshKey)
|
||||
println("txn1: ${txn1.id} spent ${((txn1.tx.outputs[0].data) as Cash.State).amount}")
|
||||
@ -187,7 +171,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
database.transaction {
|
||||
try {
|
||||
val txn2Builder = TransactionBuilder(DUMMY_NOTARY)
|
||||
vault.generateSpend(txn2Builder, 80.DOLLARS, BOB)
|
||||
Cash.generateSpend(services, txn2Builder, 80.DOLLARS, BOB)
|
||||
val ptxn2 = notaryServices.signInitialTransaction(txn2Builder)
|
||||
val txn2 = services.addSignature(ptxn2, freshKey)
|
||||
println("txn2: ${txn2.id} spent ${((txn2.tx.outputs[0].data) as Cash.State).amount}")
|
||||
@ -299,7 +283,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
|
||||
database.transaction {
|
||||
// A tx that spends our money.
|
||||
val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY)
|
||||
vault.generateSpend(spendTXBuilder, 80.DOLLARS, BOB)
|
||||
Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB)
|
||||
val spendPTX = notaryServices.signInitialTransaction(spendTXBuilder)
|
||||
val spendTX = services.addSignature(spendPTX, freshKey)
|
||||
services.recordTransactions(spendTX)
|
||||
|
@ -1,19 +1,28 @@
|
||||
package net.corda.core.crypto
|
||||
package net.corda.node.utilities
|
||||
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.Crypto.EDDSA_ED25519_SHA512
|
||||
import net.corda.core.crypto.Crypto.generateKeyPair
|
||||
import net.corda.core.crypto.X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME
|
||||
import net.corda.core.crypto.X509Utilities.createSelfSignedCACertificate
|
||||
import net.corda.core.crypto.cert
|
||||
import net.corda.core.crypto.commonName
|
||||
import net.corda.core.crypto.getX509Name
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.toTypedArray
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.node.serialization.KryoServerSerializationScheme
|
||||
import net.corda.node.services.config.createKeystoreForCordaNode
|
||||
import net.corda.node.utilities.*
|
||||
import net.corda.testing.MEGA_CORP
|
||||
import net.corda.testing.getTestX509Name
|
||||
import net.corda.nodeapi.internal.serialization.AllWhitelist
|
||||
import net.corda.nodeapi.internal.serialization.KryoHeaderV0_1
|
||||
import net.corda.nodeapi.internal.serialization.SerializationContextImpl
|
||||
import net.corda.nodeapi.internal.serialization.SerializationFactoryImpl
|
||||
import net.corda.testing.*
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.BasicConstraints
|
||||
import org.bouncycastle.asn1.x509.Extension
|
||||
import org.bouncycastle.asn1.x509.KeyUsage
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@ -27,7 +36,9 @@ import java.nio.file.Path
|
||||
import java.security.KeyStore
|
||||
import java.security.PrivateKey
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import java.util.stream.Stream
|
||||
@ -42,8 +53,8 @@ class X509UtilitiesTest {
|
||||
|
||||
@Test
|
||||
fun `create valid self-signed CA certificate`() {
|
||||
val caKey = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val caCert = createSelfSignedCACertificate(getTestX509Name("Test Cert"), caKey)
|
||||
val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val caCert = X509Utilities.createSelfSignedCACertificate(getTestX509Name("Test Cert"), caKey)
|
||||
assertTrue { caCert.subject.commonName == "Test Cert" } // using our subject common name
|
||||
assertEquals(caCert.issuer, caCert.subject) //self-signed
|
||||
caCert.isValidOn(Date()) // throws on verification problems
|
||||
@ -57,8 +68,8 @@ class X509UtilitiesTest {
|
||||
@Test
|
||||
fun `load and save a PEM file certificate`() {
|
||||
val tmpCertificateFile = tempFile("cacert.pem")
|
||||
val caKey = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val caCert = createSelfSignedCACertificate(getTestX509Name("Test Cert"), caKey)
|
||||
val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val caCert = X509Utilities.createSelfSignedCACertificate(getTestX509Name("Test Cert"), caKey)
|
||||
X509Utilities.saveCertificateAsPEMFile(caCert, tmpCertificateFile)
|
||||
val readCertificate = X509Utilities.loadCertificateFromPEMFile(tmpCertificateFile)
|
||||
assertEquals(caCert, readCertificate)
|
||||
@ -66,10 +77,10 @@ class X509UtilitiesTest {
|
||||
|
||||
@Test
|
||||
fun `create valid server certificate chain`() {
|
||||
val caKey = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val caCert = createSelfSignedCACertificate(getTestX509Name("Test CA Cert"), caKey)
|
||||
val caKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val caCert = X509Utilities.createSelfSignedCACertificate(getTestX509Name("Test CA Cert"), caKey)
|
||||
val subject = getTestX509Name("Server Cert")
|
||||
val keyPair = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val keyPair = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val serverCert = X509Utilities.createCertificate(CertificateType.TLS, caCert, caKey, subject, keyPair.public)
|
||||
assertTrue { serverCert.subject.toString().contains("CN=Server Cert") } // using our subject common name
|
||||
assertEquals(caCert.issuer, serverCert.issuer) // Issued by our CA cert
|
||||
@ -86,7 +97,7 @@ class X509UtilitiesTest {
|
||||
val tmpKeyStore = tempFile("keystore.jks")
|
||||
|
||||
val keyPair = generateKeyPair(EDDSA_ED25519_SHA512)
|
||||
val selfSignCert = createSelfSignedCACertificate(X500Name("CN=Test"), keyPair)
|
||||
val selfSignCert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Test"), keyPair)
|
||||
|
||||
assertTrue(Arrays.equals(selfSignCert.subjectPublicKeyInfo.encoded, keyPair.public.encoded))
|
||||
|
||||
@ -111,7 +122,7 @@ class X509UtilitiesTest {
|
||||
fun `signing EdDSA key with EcDSA certificate`() {
|
||||
val tmpKeyStore = tempFile("keystore.jks")
|
||||
val ecDSAKey = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
|
||||
val ecDSACert = createSelfSignedCACertificate(X500Name("CN=Test"), ecDSAKey)
|
||||
val ecDSACert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Test"), ecDSAKey)
|
||||
val edDSAKeypair = generateKeyPair(EDDSA_ED25519_SHA512)
|
||||
val edDSACert = X509Utilities.createCertificate(CertificateType.TLS, ecDSACert, ecDSAKey, X500Name("CN=TestEdDSA"), edDSAKeypair.public)
|
||||
|
||||
@ -155,8 +166,8 @@ class X509UtilitiesTest {
|
||||
|
||||
// Now sign something with private key and verify against certificate public key
|
||||
val testData = "12345".toByteArray()
|
||||
val caSignature = Crypto.doSign(DEFAULT_TLS_SIGNATURE_SCHEME, rootCaPrivateKey, testData)
|
||||
assertTrue { Crypto.isValid(DEFAULT_TLS_SIGNATURE_SCHEME, rootCaCert.publicKey, caSignature, testData) }
|
||||
val caSignature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, rootCaPrivateKey, testData)
|
||||
assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, rootCaCert.publicKey, caSignature, testData) }
|
||||
|
||||
// Load back generated intermediate CA Cert and private key
|
||||
val intermediateCaCert = keyStore.getCertificate(X509Utilities.CORDA_INTERMEDIATE_CA) as X509Certificate
|
||||
@ -165,8 +176,8 @@ class X509UtilitiesTest {
|
||||
intermediateCaCert.verify(rootCaCert.publicKey)
|
||||
|
||||
// Now sign something with private key and verify against certificate public key
|
||||
val intermediateSignature = Crypto.doSign(DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCertPrivateKey, testData)
|
||||
assertTrue { Crypto.isValid(DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCert.publicKey, intermediateSignature, testData) }
|
||||
val intermediateSignature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCertPrivateKey, testData)
|
||||
assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, intermediateCaCert.publicKey, intermediateSignature, testData) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -209,9 +220,9 @@ class X509UtilitiesTest {
|
||||
assertTrue { sslCertAndKey.certificate.subject.toString().contains(MEGA_CORP.name.commonName) }
|
||||
// Now sign something with private key and verify against certificate public key
|
||||
val testData = "123456".toByteArray()
|
||||
val signature = Crypto.doSign(DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.keyPair.private, testData)
|
||||
val signature = Crypto.doSign(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.keyPair.private, testData)
|
||||
val publicKey = Crypto.toSupportedPublicKey(serverCertAndKey.certificate.subjectPublicKeyInfo)
|
||||
assertTrue { Crypto.isValid(DEFAULT_TLS_SIGNATURE_SCHEME, publicKey, signature, testData) }
|
||||
assertTrue { Crypto.isValid(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME, publicKey, signature, testData) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -343,11 +354,11 @@ class X509UtilitiesTest {
|
||||
trustStoreFilePath: Path,
|
||||
trustStorePassword: String
|
||||
): KeyStore {
|
||||
val rootCAKey = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val rootCACert = createSelfSignedCACertificate(X509Utilities.getX509Name("Corda Node Root CA","London","demo@r3.com",null), rootCAKey)
|
||||
val rootCAKey = generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val rootCACert = X509Utilities.createSelfSignedCACertificate(getX509Name("Corda Node Root CA", "London", "demo@r3.com", null), rootCAKey)
|
||||
|
||||
val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X509Utilities.getX509Name("Corda Node Intermediate CA","London","demo@r3.com",null), intermediateCAKeyPair.public)
|
||||
val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, getX509Name("Corda Node Intermediate CA", "London", "demo@r3.com", null), intermediateCAKeyPair.public)
|
||||
|
||||
val keyPass = keyPassword.toCharArray()
|
||||
val keyStore = loadOrCreateKeyStore(keyStoreFilePath, storePassword)
|
||||
@ -374,7 +385,7 @@ class X509UtilitiesTest {
|
||||
@Test
|
||||
fun `Get correct private key type from Keystore`() {
|
||||
val keyPair = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
|
||||
val selfSignCert = createSelfSignedCACertificate(X500Name("CN=Test"), keyPair)
|
||||
val selfSignCert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Test"), keyPair)
|
||||
val keyStore = loadOrCreateKeyStore(tempFile("testKeystore.jks"), "keystorepassword")
|
||||
keyStore.setKeyEntry("Key", keyPair.private, "keypassword".toCharArray(), arrayOf(selfSignCert.cert))
|
||||
|
||||
@ -384,4 +395,38 @@ class X509UtilitiesTest {
|
||||
assertTrue(keyFromKeystore is java.security.interfaces.ECPrivateKey) // by default JKS returns SUN EC key
|
||||
assertTrue(keyFromKeystoreCasted is org.bouncycastle.jce.interfaces.ECPrivateKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize - deserialize X509CertififcateHolder`() {
|
||||
val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) }
|
||||
val context = SerializationContextImpl(KryoHeaderV0_1,
|
||||
javaClass.classLoader,
|
||||
AllWhitelist,
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.P2P)
|
||||
val expected: X509CertificateHolder = X509Utilities.createSelfSignedCACertificate(ALICE.name, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME))
|
||||
val serialized = expected.serialize(factory, context).bytes
|
||||
val actual: X509CertificateHolder = serialized.deserialize(factory, context)
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize - deserialize X509CertPath`() {
|
||||
val factory = SerializationFactoryImpl().apply { registerScheme(KryoServerSerializationScheme()) }
|
||||
val context = SerializationContextImpl(KryoHeaderV0_1,
|
||||
javaClass.classLoader,
|
||||
AllWhitelist,
|
||||
emptyMap(),
|
||||
true,
|
||||
SerializationContext.UseCase.P2P)
|
||||
val certFactory = CertificateFactory.getInstance("X509")
|
||||
val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val rootCACert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootCAKey)
|
||||
val certificate = X509Utilities.createCertificate(CertificateType.TLS, rootCACert, rootCAKey, BOB.name, BOB_PUBKEY)
|
||||
val expected = certFactory.generateCertPath(listOf(certificate.cert, rootCACert.cert))
|
||||
val serialized = expected.serialize(factory, context).bytes
|
||||
val actual: CertPath = serialized.deserialize(factory, context)
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
}
|
@ -3,9 +3,13 @@ package net.corda.node.utilities.registration
|
||||
import com.nhaarman.mockito_kotlin.any
|
||||
import com.nhaarman.mockito_kotlin.eq
|
||||
import com.nhaarman.mockito_kotlin.mock
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.cert
|
||||
import net.corda.core.crypto.commonName
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.core.internal.toTypedArray
|
||||
import net.corda.node.utilities.X509Utilities
|
||||
import net.corda.node.utilities.loadKeyStore
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.getTestX509Name
|
||||
|
@ -3,7 +3,6 @@ package net.corda.irs.contract
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import net.corda.contracts.*
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.clauses.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.containsAny
|
||||
import net.corda.core.flows.FlowLogicRefFactory
|
||||
@ -460,189 +459,137 @@ class InterestRateSwap : Contract {
|
||||
fixingCalendar, index, indexSource, indexTenor)
|
||||
}
|
||||
|
||||
override fun verify(tx: LedgerTransaction) = verifyClause(tx, AllOf(Clauses.TimeWindow(), Clauses.Group()), tx.commands.select<Commands>())
|
||||
// These functions may make more sense to use for basket types, but for now let's leave them here
|
||||
private fun checkLegDates(legs: List<CommonLeg>) {
|
||||
requireThat {
|
||||
"Effective date is before termination date" using legs.all { it.effectiveDate < it.terminationDate }
|
||||
"Effective dates are in alignment" using legs.all { it.effectiveDate == legs[0].effectiveDate }
|
||||
"Termination dates are in alignment" using legs.all { it.terminationDate == legs[0].terminationDate }
|
||||
}
|
||||
}
|
||||
|
||||
interface Clauses {
|
||||
/**
|
||||
* Common superclass for IRS contract clauses, which defines behaviour on match/no-match, and provides
|
||||
* helper functions for the clauses.
|
||||
*/
|
||||
abstract class AbstractIRSClause : Clause<State, Commands, UniqueIdentifier>() {
|
||||
// These functions may make more sense to use for basket types, but for now let's leave them here
|
||||
fun checkLegDates(legs: List<CommonLeg>) {
|
||||
private fun checkLegAmounts(legs: List<CommonLeg>) {
|
||||
requireThat {
|
||||
"The notional is non zero" using legs.any { it.notional.quantity > (0).toLong() }
|
||||
"The notional for all legs must be the same" using legs.all { it.notional == legs[0].notional }
|
||||
}
|
||||
for (leg: CommonLeg in legs) {
|
||||
if (leg is FixedLeg) {
|
||||
requireThat {
|
||||
"Effective date is before termination date" using legs.all { it.effectiveDate < it.terminationDate }
|
||||
"Effective dates are in alignment" using legs.all { it.effectiveDate == legs[0].effectiveDate }
|
||||
"Termination dates are in alignment" using legs.all { it.terminationDate == legs[0].terminationDate }
|
||||
}
|
||||
}
|
||||
|
||||
fun checkLegAmounts(legs: List<CommonLeg>) {
|
||||
requireThat {
|
||||
"The notional is non zero" using legs.any { it.notional.quantity > (0).toLong() }
|
||||
"The notional for all legs must be the same" using legs.all { it.notional == legs[0].notional }
|
||||
}
|
||||
for (leg: CommonLeg in legs) {
|
||||
if (leg is FixedLeg) {
|
||||
requireThat {
|
||||
// TODO: Confirm: would someone really enter a swap with a negative fixed rate?
|
||||
"Fixed leg rate must be positive" using leg.fixedRate.isPositive()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: After business rules discussion, add further checks to the schedules and rates
|
||||
fun checkSchedules(@Suppress("UNUSED_PARAMETER") legs: List<CommonLeg>): Boolean = true
|
||||
|
||||
fun checkRates(@Suppress("UNUSED_PARAMETER") legs: List<CommonLeg>): Boolean = true
|
||||
|
||||
/**
|
||||
* Compares two schedules of Floating Leg Payments, returns the difference (i.e. omissions in either leg or changes to the values).
|
||||
*/
|
||||
fun getFloatingLegPaymentsDifferences(payments1: Map<LocalDate, Event>, payments2: Map<LocalDate, Event>): List<Pair<LocalDate, Pair<FloatingRatePaymentEvent, FloatingRatePaymentEvent>>> {
|
||||
val diff1 = payments1.filter { payments1[it.key] != payments2[it.key] }
|
||||
val diff2 = payments2.filter { payments1[it.key] != payments2[it.key] }
|
||||
return (diff1.keys + diff2.keys).map {
|
||||
it to Pair(diff1[it] as FloatingRatePaymentEvent, diff2[it] as FloatingRatePaymentEvent)
|
||||
// TODO: Confirm: would someone really enter a swap with a negative fixed rate?
|
||||
"Fixed leg rate must be positive" using leg.fixedRate.isPositive()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Group : GroupClauseVerifier<State, Commands, UniqueIdentifier>(AnyOf(Agree(), Fix(), Pay(), Mature())) {
|
||||
// Group by Trade ID for in / out states
|
||||
override fun groupStates(tx: LedgerTransaction): List<LedgerTransaction.InOutGroup<State, UniqueIdentifier>> {
|
||||
return tx.groupStates { state -> state.linearId }
|
||||
}
|
||||
/**
|
||||
* Compares two schedules of Floating Leg Payments, returns the difference (i.e. omissions in either leg or changes to the values).
|
||||
*/
|
||||
private fun getFloatingLegPaymentsDifferences(payments1: Map<LocalDate, Event>, payments2: Map<LocalDate, Event>): List<Pair<LocalDate, Pair<FloatingRatePaymentEvent, FloatingRatePaymentEvent>>> {
|
||||
val diff1 = payments1.filter { payments1[it.key] != payments2[it.key] }
|
||||
val diff2 = payments2.filter { payments1[it.key] != payments2[it.key] }
|
||||
return (diff1.keys + diff2.keys).map {
|
||||
it to Pair(diff1[it] as FloatingRatePaymentEvent, diff2[it] as FloatingRatePaymentEvent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyAgreeCommand(inputs: List<State>, outputs: List<State>) {
|
||||
val irs = outputs.filterIsInstance<State>().single()
|
||||
requireThat {
|
||||
"There are no in states for an agreement" using inputs.isEmpty()
|
||||
"There are events in the fix schedule" using (irs.calculation.fixedLegPaymentSchedule.isNotEmpty())
|
||||
"There are events in the float schedule" using (irs.calculation.floatingLegPaymentSchedule.isNotEmpty())
|
||||
"All notionals must be non zero" using (irs.fixedLeg.notional.quantity > 0 && irs.floatingLeg.notional.quantity > 0)
|
||||
"The fixed leg rate must be positive" using (irs.fixedLeg.fixedRate.isPositive())
|
||||
"The currency of the notionals must be the same" using (irs.fixedLeg.notional.token == irs.floatingLeg.notional.token)
|
||||
"All leg notionals must be the same" using (irs.fixedLeg.notional == irs.floatingLeg.notional)
|
||||
"The effective date is before the termination date for the fixed leg" using (irs.fixedLeg.effectiveDate < irs.fixedLeg.terminationDate)
|
||||
"The effective date is before the termination date for the floating leg" using (irs.floatingLeg.effectiveDate < irs.floatingLeg.terminationDate)
|
||||
"The effective dates are aligned" using (irs.floatingLeg.effectiveDate == irs.fixedLeg.effectiveDate)
|
||||
"The termination dates are aligned" using (irs.floatingLeg.terminationDate == irs.fixedLeg.terminationDate)
|
||||
"The fixing period date offset cannot be negative" using (irs.floatingLeg.fixingPeriodOffset >= 0)
|
||||
|
||||
// TODO: further tests
|
||||
}
|
||||
checkLegAmounts(listOf(irs.fixedLeg, irs.floatingLeg))
|
||||
checkLegDates(listOf(irs.fixedLeg, irs.floatingLeg))
|
||||
}
|
||||
|
||||
private fun verifyFixCommand(inputs: List<State>, outputs: List<State>, command: AuthenticatedObject<Commands.Refix>) {
|
||||
val irs = outputs.filterIsInstance<State>().single()
|
||||
val prevIrs = inputs.filterIsInstance<State>().single()
|
||||
val paymentDifferences = getFloatingLegPaymentsDifferences(prevIrs.calculation.floatingLegPaymentSchedule, irs.calculation.floatingLegPaymentSchedule)
|
||||
|
||||
// Having both of these tests are "redundant" as far as verify() goes, however, by performing both
|
||||
// we can relay more information back to the user in the case of failure.
|
||||
requireThat {
|
||||
"There is at least one difference in the IRS floating leg payment schedules" using !paymentDifferences.isEmpty()
|
||||
"There is only one change in the IRS floating leg payment schedule" using (paymentDifferences.size == 1)
|
||||
}
|
||||
|
||||
class TimeWindow : Clause<ContractState, Commands, Unit>() {
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Unit?): Set<Commands> {
|
||||
requireNotNull(tx.timeWindow) { "must be have a time-window)" }
|
||||
// We return an empty set because we don't process any commands
|
||||
return emptySet()
|
||||
val (oldFloatingRatePaymentEvent, newFixedRatePaymentEvent) = paymentDifferences.single().second // Ignore the date of the changed rate (we checked that earlier).
|
||||
val fixValue = command.value.fix
|
||||
// Need to check that everything is the same apart from the new fixed rate entry.
|
||||
requireThat {
|
||||
"The fixed leg parties are constant" using (irs.fixedLeg.fixedRatePayer == prevIrs.fixedLeg.fixedRatePayer) // Although superseded by the below test, this is included for a regression issue
|
||||
"The fixed leg is constant" using (irs.fixedLeg == prevIrs.fixedLeg)
|
||||
"The floating leg is constant" using (irs.floatingLeg == prevIrs.floatingLeg)
|
||||
"The common values are constant" using (irs.common == prevIrs.common)
|
||||
"The fixed leg payment schedule is constant" using (irs.calculation.fixedLegPaymentSchedule == prevIrs.calculation.fixedLegPaymentSchedule)
|
||||
"The expression is unchanged" using (irs.calculation.expression == prevIrs.calculation.expression)
|
||||
"There is only one changed payment in the floating leg" using (paymentDifferences.size == 1)
|
||||
"There changed payment is a floating payment" using (oldFloatingRatePaymentEvent.rate is ReferenceRate)
|
||||
"The new payment is a fixed payment" using (newFixedRatePaymentEvent.rate is FixedRate)
|
||||
"The changed payments dates are aligned" using (oldFloatingRatePaymentEvent.date == newFixedRatePaymentEvent.date)
|
||||
"The new payment has the correct rate" using (newFixedRatePaymentEvent.rate.ratioUnit!!.value == fixValue.value)
|
||||
"The fixing is for the next required date" using (prevIrs.calculation.nextFixingDate() == fixValue.of.forDay)
|
||||
"The fix payment has the same currency as the notional" using (newFixedRatePaymentEvent.flow.token == irs.floatingLeg.notional.token)
|
||||
// "The fixing is not in the future " by (fixCommand) // The oracle should not have signed this .
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyPayCommand() {
|
||||
requireThat {
|
||||
"Payments not supported / verifiable yet" using false
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyMatureCommand(inputs: List<State>, outputs: List<State>) {
|
||||
val irs = inputs.filterIsInstance<State>().single()
|
||||
requireThat {
|
||||
"No more fixings to be applied" using (irs.calculation.nextFixingDate() == null)
|
||||
"The irs is fully consumed and there is no id matched output state" using outputs.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
requireNotNull(tx.timeWindow) { "must be have a time-window)" }
|
||||
val groups: List<LedgerTransaction.InOutGroup<State, UniqueIdentifier>> = tx.groupStates { state -> state.linearId }
|
||||
var atLeastOneCommandProcessed = false
|
||||
for ((inputs, outputs, key) in groups) {
|
||||
val agreeCommand = tx.commands.select<Commands.Agree>().firstOrNull()
|
||||
if (agreeCommand != null) {
|
||||
verifyAgreeCommand(inputs, outputs)
|
||||
atLeastOneCommandProcessed = true
|
||||
}
|
||||
val fixCommand = tx.commands.select<Commands.Refix>().firstOrNull()
|
||||
if (fixCommand != null) {
|
||||
verifyFixCommand(inputs, outputs, fixCommand)
|
||||
atLeastOneCommandProcessed = true
|
||||
}
|
||||
val payCommand = tx.commands.select<Commands.Pay>().firstOrNull()
|
||||
if (payCommand != null) {
|
||||
verifyPayCommand()
|
||||
atLeastOneCommandProcessed = true
|
||||
}
|
||||
val matureCommand = tx.commands.select<Commands.Mature>().firstOrNull()
|
||||
if (matureCommand != null) {
|
||||
verifyMatureCommand(inputs, outputs)
|
||||
atLeastOneCommandProcessed = true
|
||||
}
|
||||
}
|
||||
|
||||
class Agree : AbstractIRSClause() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Agree::class.java)
|
||||
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Agree>()
|
||||
val irs = outputs.filterIsInstance<State>().single()
|
||||
requireThat {
|
||||
"There are no in states for an agreement" using inputs.isEmpty()
|
||||
"There are events in the fix schedule" using (irs.calculation.fixedLegPaymentSchedule.isNotEmpty())
|
||||
"There are events in the float schedule" using (irs.calculation.floatingLegPaymentSchedule.isNotEmpty())
|
||||
"All notionals must be non zero" using (irs.fixedLeg.notional.quantity > 0 && irs.floatingLeg.notional.quantity > 0)
|
||||
"The fixed leg rate must be positive" using (irs.fixedLeg.fixedRate.isPositive())
|
||||
"The currency of the notionals must be the same" using (irs.fixedLeg.notional.token == irs.floatingLeg.notional.token)
|
||||
"All leg notionals must be the same" using (irs.fixedLeg.notional == irs.floatingLeg.notional)
|
||||
|
||||
"The effective date is before the termination date for the fixed leg" using (irs.fixedLeg.effectiveDate < irs.fixedLeg.terminationDate)
|
||||
"The effective date is before the termination date for the floating leg" using (irs.floatingLeg.effectiveDate < irs.floatingLeg.terminationDate)
|
||||
"The effective dates are aligned" using (irs.floatingLeg.effectiveDate == irs.fixedLeg.effectiveDate)
|
||||
"The termination dates are aligned" using (irs.floatingLeg.terminationDate == irs.fixedLeg.terminationDate)
|
||||
"The rates are valid" using checkRates(listOf(irs.fixedLeg, irs.floatingLeg))
|
||||
"The schedules are valid" using checkSchedules(listOf(irs.fixedLeg, irs.floatingLeg))
|
||||
"The fixing period date offset cannot be negative" using (irs.floatingLeg.fixingPeriodOffset >= 0)
|
||||
|
||||
// TODO: further tests
|
||||
}
|
||||
checkLegAmounts(listOf(irs.fixedLeg, irs.floatingLeg))
|
||||
checkLegDates(listOf(irs.fixedLeg, irs.floatingLeg))
|
||||
|
||||
return setOf(command.value)
|
||||
}
|
||||
}
|
||||
|
||||
class Fix : AbstractIRSClause() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Refix::class.java)
|
||||
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Refix>()
|
||||
val irs = outputs.filterIsInstance<State>().single()
|
||||
val prevIrs = inputs.filterIsInstance<State>().single()
|
||||
val paymentDifferences = getFloatingLegPaymentsDifferences(prevIrs.calculation.floatingLegPaymentSchedule, irs.calculation.floatingLegPaymentSchedule)
|
||||
|
||||
// Having both of these tests are "redundant" as far as verify() goes, however, by performing both
|
||||
// we can relay more information back to the user in the case of failure.
|
||||
requireThat {
|
||||
"There is at least one difference in the IRS floating leg payment schedules" using !paymentDifferences.isEmpty()
|
||||
"There is only one change in the IRS floating leg payment schedule" using (paymentDifferences.size == 1)
|
||||
}
|
||||
|
||||
val (oldFloatingRatePaymentEvent, newFixedRatePaymentEvent) = paymentDifferences.single().second // Ignore the date of the changed rate (we checked that earlier).
|
||||
val fixValue = command.value.fix
|
||||
// Need to check that everything is the same apart from the new fixed rate entry.
|
||||
requireThat {
|
||||
"The fixed leg parties are constant" using (irs.fixedLeg.fixedRatePayer == prevIrs.fixedLeg.fixedRatePayer) // Although superseded by the below test, this is included for a regression issue
|
||||
"The fixed leg is constant" using (irs.fixedLeg == prevIrs.fixedLeg)
|
||||
"The floating leg is constant" using (irs.floatingLeg == prevIrs.floatingLeg)
|
||||
"The common values are constant" using (irs.common == prevIrs.common)
|
||||
"The fixed leg payment schedule is constant" using (irs.calculation.fixedLegPaymentSchedule == prevIrs.calculation.fixedLegPaymentSchedule)
|
||||
"The expression is unchanged" using (irs.calculation.expression == prevIrs.calculation.expression)
|
||||
"There is only one changed payment in the floating leg" using (paymentDifferences.size == 1)
|
||||
"There changed payment is a floating payment" using (oldFloatingRatePaymentEvent.rate is ReferenceRate)
|
||||
"The new payment is a fixed payment" using (newFixedRatePaymentEvent.rate is FixedRate)
|
||||
"The changed payments dates are aligned" using (oldFloatingRatePaymentEvent.date == newFixedRatePaymentEvent.date)
|
||||
"The new payment has the correct rate" using (newFixedRatePaymentEvent.rate.ratioUnit!!.value == fixValue.value)
|
||||
"The fixing is for the next required date" using (prevIrs.calculation.nextFixingDate() == fixValue.of.forDay)
|
||||
"The fix payment has the same currency as the notional" using (newFixedRatePaymentEvent.flow.token == irs.floatingLeg.notional.token)
|
||||
// "The fixing is not in the future " by (fixCommand) // The oracle should not have signed this .
|
||||
}
|
||||
|
||||
return setOf(command.value)
|
||||
}
|
||||
}
|
||||
|
||||
class Pay : AbstractIRSClause() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Pay::class.java)
|
||||
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Pay>()
|
||||
requireThat {
|
||||
"Payments not supported / verifiable yet" using false
|
||||
}
|
||||
return setOf(command.value)
|
||||
}
|
||||
}
|
||||
|
||||
class Mature : AbstractIRSClause() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Mature::class.java)
|
||||
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<State>,
|
||||
outputs: List<State>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Mature>()
|
||||
val irs = inputs.filterIsInstance<State>().single()
|
||||
requireThat {
|
||||
"No more fixings to be applied" using (irs.calculation.nextFixingDate() == null)
|
||||
"The irs is fully consumed and there is no id matched output state" using outputs.isEmpty()
|
||||
}
|
||||
|
||||
return setOf(command.value)
|
||||
}
|
||||
}
|
||||
|
||||
require(atLeastOneCommandProcessed) { "At least one command needs to present" }
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
|
@ -34,7 +34,7 @@ class DummyIssueAndMove(private val notary: Party, private val counterpartyNode:
|
||||
// Move ownership of the asset to the counterparty
|
||||
val moveTxBuilder = TransactionBuilder(notary = notary)
|
||||
|
||||
val (_, keys) = vaultService.generateSpend(moveTxBuilder, Amount(amount.quantity, GBP), counterpartyNode)
|
||||
val (_, keys) = Cash.generateSpend(serviceHub, moveTxBuilder, Amount(amount.quantity, GBP), counterpartyNode)
|
||||
// We don't check signatures because we know that the notary's signature is missing
|
||||
signInitialTransaction(moveTxBuilder, keys)
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.corda.vega.contracts
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.clauses.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import java.math.BigDecimal
|
||||
@ -10,39 +9,13 @@ import java.math.BigDecimal
|
||||
* Specifies the contract between two parties that trade an OpenGamma IRS. Currently can only agree to trade.
|
||||
*/
|
||||
data class OGTrade(override val legalContractReference: SecureHash = SecureHash.sha256("OGTRADE.KT")) : Contract {
|
||||
override fun verify(tx: LedgerTransaction) = verifyClause(tx, AllOf(Clauses.TimeWindowed(), Clauses.Group()), tx.commands.select<Commands>())
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Agree : TypeOnlyCommandData(), Commands // Both sides agree to trade
|
||||
}
|
||||
|
||||
interface Clauses {
|
||||
class TimeWindowed : Clause<ContractState, Commands, Unit>() {
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Unit?): Set<Commands> {
|
||||
requireNotNull(tx.timeWindow) { "must have a time-window" }
|
||||
// We return an empty set because we don't process any commands
|
||||
return emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
class Group : GroupClauseVerifier<IRSState, Commands, UniqueIdentifier>(AnyOf(Agree())) {
|
||||
override fun groupStates(tx: LedgerTransaction): List<LedgerTransaction.InOutGroup<IRSState, UniqueIdentifier>>
|
||||
// Group by Trade ID for in / out states
|
||||
= tx.groupStates { state -> state.linearId }
|
||||
}
|
||||
|
||||
class Agree : Clause<IRSState, Commands, UniqueIdentifier>() {
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<IRSState>,
|
||||
outputs: List<IRSState>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Agree>()
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
requireNotNull(tx.timeWindow) { "must have a time-window" }
|
||||
val groups: List<LedgerTransaction.InOutGroup<IRSState, UniqueIdentifier>> = tx.groupStates { state -> state.linearId }
|
||||
var atLeastOneCommandProcessed = false
|
||||
for ((inputs, outputs, key) in groups) {
|
||||
val command = tx.commands.select<Commands.Agree>().firstOrNull()
|
||||
if (command != null) {
|
||||
require(inputs.isEmpty()) { "Inputs must be empty" }
|
||||
require(outputs.size == 1) { "" }
|
||||
require(outputs[0].buyer != outputs[0].seller)
|
||||
@ -51,9 +24,13 @@ data class OGTrade(override val legalContractReference: SecureHash = SecureHash.
|
||||
require(outputs[0].swap.startDate.isBefore(outputs[0].swap.endDate))
|
||||
require(outputs[0].swap.notional > BigDecimal(0))
|
||||
require(outputs[0].swap.tradeDate.isBefore(outputs[0].swap.endDate))
|
||||
|
||||
return setOf(command.value)
|
||||
atLeastOneCommandProcessed = true
|
||||
}
|
||||
}
|
||||
require(atLeastOneCommandProcessed) { "At least one command needs to present" }
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Agree : TypeOnlyCommandData(), Commands // Both sides agree to trade
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.corda.vega.contracts
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.clauses.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
|
||||
@ -11,71 +10,34 @@ import net.corda.core.transactions.LedgerTransaction
|
||||
* of the portfolio arbitrarily.
|
||||
*/
|
||||
data class PortfolioSwap(override val legalContractReference: SecureHash = SecureHash.sha256("swordfish")) : Contract {
|
||||
override fun verify(tx: LedgerTransaction) = verifyClause(tx, AllOf(Clauses.TimeWindowed(), Clauses.Group()), tx.commands.select<Commands>())
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Agree : TypeOnlyCommandData(), Commands // Both sides agree to portfolio
|
||||
class Update : TypeOnlyCommandData(), Commands // Both sides re-agree to portfolio
|
||||
}
|
||||
|
||||
interface Clauses {
|
||||
class TimeWindowed : Clause<ContractState, Commands, Unit>() {
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<ContractState>,
|
||||
outputs: List<ContractState>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: Unit?): Set<Commands> {
|
||||
requireNotNull(tx.timeWindow) { "must have a time-window)" }
|
||||
// We return an empty set because we don't process any commands
|
||||
return emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
class Group : GroupClauseVerifier<PortfolioState, Commands, UniqueIdentifier>(FirstOf(Agree(), Update())) {
|
||||
override fun groupStates(tx: LedgerTransaction): List<LedgerTransaction.InOutGroup<PortfolioState, UniqueIdentifier>>
|
||||
// Group by Trade ID for in / out states
|
||||
= tx.groupStates { state -> state.linearId }
|
||||
}
|
||||
|
||||
class Update : Clause<PortfolioState, Commands, UniqueIdentifier>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Update::class.java)
|
||||
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<PortfolioState>,
|
||||
outputs: List<PortfolioState>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Update>()
|
||||
|
||||
requireThat {
|
||||
"there is only one input" using (inputs.size == 1)
|
||||
"there is only one output" using (outputs.size == 1)
|
||||
"the valuer hasn't changed" using (inputs[0].valuer == outputs[0].valuer)
|
||||
"the linear id hasn't changed" using (inputs[0].linearId == outputs[0].linearId)
|
||||
}
|
||||
|
||||
return setOf(command.value)
|
||||
}
|
||||
}
|
||||
|
||||
class Agree : Clause<PortfolioState, Commands, UniqueIdentifier>() {
|
||||
override val requiredCommands: Set<Class<out CommandData>> = setOf(Commands.Agree::class.java)
|
||||
|
||||
override fun verify(tx: LedgerTransaction,
|
||||
inputs: List<PortfolioState>,
|
||||
outputs: List<PortfolioState>,
|
||||
commands: List<AuthenticatedObject<Commands>>,
|
||||
groupingKey: UniqueIdentifier?): Set<Commands> {
|
||||
val command = tx.commands.requireSingleCommand<Commands.Agree>()
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
requireNotNull(tx.timeWindow) { "must have a time-window)" }
|
||||
val groups: List<LedgerTransaction.InOutGroup<PortfolioState, UniqueIdentifier>> = tx.groupStates { state -> state.linearId }
|
||||
for ((inputs, outputs, key) in groups) {
|
||||
val agreeCommand = tx.commands.select<Commands.Agree>().firstOrNull()
|
||||
if (agreeCommand != null) {
|
||||
requireThat {
|
||||
"there are no inputs" using (inputs.isEmpty())
|
||||
"there is one output" using (outputs.size == 1)
|
||||
"valuer must be a party" using (outputs[0].participants.contains(outputs[0].valuer))
|
||||
}
|
||||
} else {
|
||||
val updateCommand = tx.commands.select<Commands.Update>().firstOrNull()
|
||||
if (updateCommand != null) {
|
||||
requireThat {
|
||||
"there is only one input" using (inputs.size == 1)
|
||||
"there is only one output" using (outputs.size == 1)
|
||||
"the valuer hasn't changed" using (inputs[0].valuer == outputs[0].valuer)
|
||||
"the linear id hasn't changed" using (inputs[0].linearId == outputs[0].linearId)
|
||||
}
|
||||
|
||||
return setOf(command.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
class Agree : TypeOnlyCommandData(), Commands // Both sides agree to portfolio
|
||||
class Update : TypeOnlyCommandData(), Commands // Both sides re-agree to portfolio
|
||||
}
|
||||
}
|
||||
|
@ -40,8 +40,9 @@ dependencies {
|
||||
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
ext.rpcUsers = [['username': "demo", 'password': "demo", 'permissions': [
|
||||
'StartFlow.net.corda.flows.IssuerFlow$IssuanceRequester',
|
||||
"StartFlow.net.corda.traderdemo.flow.SellerFlow"
|
||||
'StartFlow.net.corda.flows.CashIssueFlow',
|
||||
'StartFlow.net.corda.traderdemo.flow.CommercialPaperIssueFlow',
|
||||
'StartFlow.net.corda.traderdemo.flow.SellerFlow'
|
||||
]]]
|
||||
|
||||
directory "./build/nodes"
|
||||
@ -74,7 +75,9 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
name "CN=BankOfCorda,O=R3,L=New York,C=US"
|
||||
advertisedServices = []
|
||||
p2pPort 10011
|
||||
rpcPort 10012
|
||||
cordapps = []
|
||||
rpcUsers = ext.rpcUsers
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,11 +105,11 @@ publishing {
|
||||
}
|
||||
}
|
||||
|
||||
task runBuyer(type: JavaExec) {
|
||||
task runBank(type: JavaExec) {
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
main = 'net.corda.traderdemo.TraderDemoKt'
|
||||
args '--role'
|
||||
args 'BUYER'
|
||||
args 'BANK'
|
||||
}
|
||||
|
||||
task runSeller(type: JavaExec) {
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.utilities.millis
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.testing.DUMMY_BANK_A
|
||||
import net.corda.testing.DUMMY_BANK_B
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
@ -17,6 +18,7 @@ import net.corda.testing.BOC
|
||||
import net.corda.testing.driver.poll
|
||||
import net.corda.testing.node.NodeBasedTest
|
||||
import net.corda.traderdemo.flow.BuyerFlow
|
||||
import net.corda.traderdemo.flow.CommercialPaperIssueFlow
|
||||
import net.corda.traderdemo.flow.SellerFlow
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
@ -25,15 +27,13 @@ import java.util.concurrent.Executors
|
||||
class TraderDemoTest : NodeBasedTest() {
|
||||
@Test
|
||||
fun `runs trader demo`() {
|
||||
val permissions = setOf(
|
||||
startFlowPermission<IssuerFlow.IssuanceRequester>(),
|
||||
startFlowPermission<SellerFlow>())
|
||||
val demoUser = listOf(User("demo", "demo", permissions))
|
||||
val user = User("user1", "test", permissions = setOf(startFlowPermission<IssuerFlow.IssuanceRequester>()))
|
||||
val (nodeA, nodeB) = listOf(
|
||||
startNode(DUMMY_BANK_A.name, rpcUsers = demoUser),
|
||||
startNode(DUMMY_BANK_B.name, rpcUsers = demoUser),
|
||||
startNode(BOC.name, rpcUsers = listOf(user)),
|
||||
val demoUser = User("demo", "demo", setOf(startFlowPermission<SellerFlow>()))
|
||||
val bankUser = User("user1", "test", permissions = setOf(startFlowPermission<CashIssueFlow>(),
|
||||
startFlowPermission<CommercialPaperIssueFlow>()))
|
||||
val (nodeA, nodeB, bankNode, notaryNode) = listOf(
|
||||
startNode(DUMMY_BANK_A.name, rpcUsers = listOf(demoUser)),
|
||||
startNode(DUMMY_BANK_B.name, rpcUsers = listOf(demoUser)),
|
||||
startNode(BOC.name, rpcUsers = listOf(bankUser)),
|
||||
startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
).transpose().getOrThrow()
|
||||
|
||||
@ -41,19 +41,24 @@ class TraderDemoTest : NodeBasedTest() {
|
||||
|
||||
val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map {
|
||||
val client = CordaRPCClient(it.configuration.rpcAddress!!, initialiseSerialization = false)
|
||||
client.start(demoUser[0].username, demoUser[0].password).proxy
|
||||
client.start(demoUser.username, demoUser.password).proxy
|
||||
}
|
||||
val nodeBankRpc = let {
|
||||
val client = CordaRPCClient(bankNode.configuration.rpcAddress!!, initialiseSerialization = false)
|
||||
client.start(bankUser.username, bankUser.password).proxy
|
||||
}
|
||||
|
||||
val clientA = TraderDemoClientApi(nodeARpc)
|
||||
val clientB = TraderDemoClientApi(nodeBRpc)
|
||||
val clientBank = TraderDemoClientApi(nodeBankRpc)
|
||||
|
||||
val originalACash = clientA.cashCount // A has random number of issued amount
|
||||
val expectedBCash = clientB.cashCount + 1
|
||||
val expectedPaper = listOf(clientA.commercialPaperCount + 1, clientB.commercialPaperCount)
|
||||
|
||||
// TODO: Enable anonymisation
|
||||
clientA.runBuyer(amount = 100.DOLLARS, anonymous = false)
|
||||
clientB.runSeller(counterparty = nodeA.info.legalIdentity.name, amount = 5.DOLLARS)
|
||||
clientBank.runIssuer(amount = 100.DOLLARS, buyerName = nodeA.info.legalIdentity.name, sellerName = nodeB.info.legalIdentity.name, notaryName = notaryNode.info.legalIdentity.name)
|
||||
clientB.runSeller(buyerName = nodeA.info.legalIdentity.name, amount = 5.DOLLARS)
|
||||
|
||||
assertThat(clientA.cashCount).isGreaterThan(originalACash)
|
||||
assertThat(clientB.cashCount).isEqualTo(expectedBCash)
|
||||
|
@ -4,8 +4,10 @@ import joptsimple.OptionParser
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.contracts.DOLLARS
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.testing.DUMMY_BANK_A
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.testing.DUMMY_BANK_A
|
||||
import net.corda.testing.DUMMY_BANK_B
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import org.slf4j.Logger
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
@ -18,12 +20,18 @@ fun main(args: Array<String>) {
|
||||
|
||||
private class TraderDemo {
|
||||
enum class Role {
|
||||
BUYER,
|
||||
BANK,
|
||||
SELLER
|
||||
}
|
||||
|
||||
companion object {
|
||||
val logger: Logger = loggerFor<TraderDemo>()
|
||||
val buyerName = DUMMY_BANK_A.name
|
||||
val sellerName = DUMMY_BANK_B.name
|
||||
val notaryName = DUMMY_NOTARY.name
|
||||
val buyerRpcPort = 10006
|
||||
val sellerRpcPort = 10009
|
||||
val bankRpcPort = 10012
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
@ -41,15 +49,15 @@ private class TraderDemo {
|
||||
// What happens next depends on the role. The buyer sits around waiting for a trade to start. The seller role
|
||||
// will contact the buyer and actually make something happen.
|
||||
val role = options.valueOf(roleArg)!!
|
||||
if (role == Role.BUYER) {
|
||||
val host = NetworkHostAndPort("localhost", 10006)
|
||||
CordaRPCClient(host).start("demo", "demo").use {
|
||||
TraderDemoClientApi(it.proxy).runBuyer()
|
||||
if (role == Role.BANK) {
|
||||
val bankHost = NetworkHostAndPort("localhost", bankRpcPort)
|
||||
CordaRPCClient(bankHost).use("demo", "demo") {
|
||||
TraderDemoClientApi(it.proxy).runIssuer(1100.DOLLARS, buyerName, sellerName, notaryName)
|
||||
}
|
||||
} else {
|
||||
val host = NetworkHostAndPort("localhost", 10009)
|
||||
CordaRPCClient(host).use("demo", "demo") {
|
||||
TraderDemoClientApi(it.proxy).runSeller(1000.DOLLARS, DUMMY_BANK_A.name)
|
||||
val sellerHost = NetworkHostAndPort("localhost", sellerRpcPort)
|
||||
CordaRPCClient(sellerHost).use("demo", "demo") {
|
||||
TraderDemoClientApi(it.proxy).runSeller(1000.DOLLARS, buyerName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,11 +16,11 @@ import net.corda.core.node.services.vault.builder
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.flows.IssuerFlow.IssuanceRequester
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.node.services.vault.VaultSchemaV1
|
||||
import net.corda.testing.BOC
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.contracts.calculateRandomlySizedAmounts
|
||||
import net.corda.traderdemo.flow.CommercialPaperIssueFlow
|
||||
import net.corda.traderdemo.flow.SellerFlow
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.util.*
|
||||
@ -47,25 +47,42 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) {
|
||||
return rpc.vaultQueryBy<CommercialPaper.State>(countCriteria).otherResults.single() as Long
|
||||
}
|
||||
|
||||
fun runBuyer(amount: Amount<Currency> = 30000.DOLLARS, anonymous: Boolean = false) {
|
||||
val bankOfCordaParty = rpc.partyFromX500Name(BOC.name)
|
||||
?: throw IllegalStateException("Unable to locate ${BOC.name} in Network Map Service")
|
||||
fun runIssuer(amount: Amount<Currency> = 1100.0.DOLLARS, buyerName: X500Name, sellerName: X500Name, notaryName: X500Name) {
|
||||
val ref = OpaqueBytes.of(1)
|
||||
val buyer = rpc.partyFromX500Name(buyerName) ?: throw IllegalStateException("Don't know $buyerName")
|
||||
val seller = rpc.partyFromX500Name(sellerName) ?: throw IllegalStateException("Don't know $sellerName")
|
||||
val notaryLegalIdentity = rpc.partyFromX500Name(DUMMY_NOTARY.name)
|
||||
?: throw IllegalStateException("Unable to locate ${DUMMY_NOTARY.name} in Network Map Service")
|
||||
val notaryNode = rpc.nodeIdentityFromParty(notaryLegalIdentity)
|
||||
?: throw IllegalStateException("Unable to locate notary node in network map cache")
|
||||
val me = rpc.nodeIdentity()
|
||||
val amounts = calculateRandomlySizedAmounts(amount, 3, 10, Random())
|
||||
// issuer random amounts of currency totaling 30000.DOLLARS in parallel
|
||||
val anonymous = false
|
||||
// issue random amounts of currency up to the requested amount, in parallel
|
||||
val resultFutures = amounts.map { pennies ->
|
||||
rpc.startFlow(::IssuanceRequester, Amount(pennies, amount.token), me.legalIdentity, OpaqueBytes.of(1), bankOfCordaParty, notaryNode.notaryIdentity, anonymous).returnValue
|
||||
rpc.startFlow(::CashIssueFlow, amount.copy(quantity = pennies), OpaqueBytes.of(1), buyer, notaryNode.notaryIdentity, anonymous).returnValue
|
||||
}
|
||||
|
||||
resultFutures.transpose().getOrThrow()
|
||||
println("Cash issued to buyer")
|
||||
|
||||
// The CP sale transaction comes with a prospectus PDF, which will tag along for the ride in an
|
||||
// attachment. Make sure we have the transaction prospectus attachment loaded into our store.
|
||||
//
|
||||
// This can also be done via an HTTP upload, but here we short-circuit and do it from code.
|
||||
if (!rpc.attachmentExists(SellerFlow.PROSPECTUS_HASH)) {
|
||||
javaClass.classLoader.getResourceAsStream("bank-of-london-cp.jar").use {
|
||||
val id = rpc.uploadAttachment(it)
|
||||
check(SellerFlow.PROSPECTUS_HASH == id)
|
||||
}
|
||||
}
|
||||
|
||||
// The line below blocks and waits for the future to resolve.
|
||||
val stx = rpc.startFlow(::CommercialPaperIssueFlow, amount, ref, seller, notaryNode.notaryIdentity).returnValue.getOrThrow()
|
||||
println("Commercial paper issued to seller")
|
||||
}
|
||||
|
||||
fun runSeller(amount: Amount<Currency> = 1000.0.DOLLARS, counterparty: X500Name) {
|
||||
val otherParty = rpc.partyFromX500Name(counterparty) ?: throw IllegalStateException("Don't know $counterparty")
|
||||
fun runSeller(amount: Amount<Currency> = 1000.0.DOLLARS, buyerName: X500Name) {
|
||||
val otherParty = rpc.partyFromX500Name(buyerName) ?: throw IllegalStateException("Don't know $buyerName")
|
||||
// The seller will sell some commercial paper to the buyer, who will pay with (self issued) cash.
|
||||
//
|
||||
// The CP sale transaction comes with a prospectus PDF, which will tag along for the ride in an
|
||||
|
@ -0,0 +1,76 @@
|
||||
package net.corda.traderdemo.flow
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.CommercialPaper
|
||||
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.`issued by`
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.utilities.seconds
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Flow for the Bank of Corda node to issue some commercial paper to the seller's node, to sell to the buyer.
|
||||
*/
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class CommercialPaperIssueFlow(val amount: Amount<Currency>,
|
||||
val issueRef: OpaqueBytes,
|
||||
val recipient: Party,
|
||||
val notary: Party,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<SignedTransaction>() {
|
||||
constructor(amount: Amount<Currency>, issueRef: OpaqueBytes, recipient: Party, notary: Party) : this(amount, issueRef, recipient, notary, tracker())
|
||||
|
||||
companion object {
|
||||
val PROSPECTUS_HASH = SecureHash.parse("decd098666b9657314870e192ced0c3519c2c9d395507a238338f8d003929de9")
|
||||
object ISSUING : ProgressTracker.Step("Issuing and timestamping some commercial paper")
|
||||
fun tracker() = ProgressTracker(ISSUING)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
progressTracker.currentStep = ISSUING
|
||||
|
||||
val me = serviceHub.myInfo.legalIdentity
|
||||
val issuance: SignedTransaction = run {
|
||||
val tx = CommercialPaper().generateIssue(me.ref(issueRef), amount `issued by` me.ref(issueRef),
|
||||
Instant.now() + 10.days, notary)
|
||||
|
||||
// TODO: Consider moving these two steps below into generateIssue.
|
||||
|
||||
// Attach the prospectus.
|
||||
tx.addAttachment(serviceHub.attachments.openAttachment(PROSPECTUS_HASH)!!.id)
|
||||
|
||||
// Requesting a time-window to be set, all CP must have a validation window.
|
||||
tx.setTimeWindow(Instant.now(), 30.seconds)
|
||||
|
||||
// Sign it as ourselves.
|
||||
val stx = serviceHub.signInitialTransaction(tx)
|
||||
|
||||
subFlow(FinalityFlow(stx)).single()
|
||||
}
|
||||
|
||||
// Now make a dummy transaction that moves it to a new key, just to show that resolving dependencies works.
|
||||
val move: SignedTransaction = run {
|
||||
val builder = TransactionBuilder(notary)
|
||||
CommercialPaper().generateMove(builder, issuance.tx.outRef(0), recipient)
|
||||
val stx = serviceHub.signInitialTransaction(builder)
|
||||
subFlow(FinalityFlow(stx)).single()
|
||||
}
|
||||
|
||||
return move
|
||||
}
|
||||
|
||||
}
|
@ -2,25 +2,17 @@ package net.corda.traderdemo.flow
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.CommercialPaper
|
||||
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.flows.FinalityFlow
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.flows.TwoPartyTradeFlow
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
@InitiatingFlow
|
||||
@ -51,7 +43,8 @@ class SellerFlow(val otherParty: Party,
|
||||
|
||||
val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0]
|
||||
val cpOwnerKey = serviceHub.keyManagementService.freshKey()
|
||||
val commercialPaper = selfIssueSomeCommercialPaper(serviceHub.myInfo.legalIdentity, notary)
|
||||
val commercialPaper = serviceHub.vaultQueryService.queryBy(CommercialPaper.State::class.java).states.first()
|
||||
|
||||
|
||||
progressTracker.currentStep = TRADING
|
||||
|
||||
@ -66,39 +59,4 @@ class SellerFlow(val otherParty: Party,
|
||||
progressTracker.getChildProgressTracker(TRADING)!!)
|
||||
return subFlow(seller)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
fun selfIssueSomeCommercialPaper(ownedBy: AbstractParty, notaryNode: NodeInfo): StateAndRef<CommercialPaper.State> {
|
||||
// Make a fake company that's issued its own paper.
|
||||
val party = Party(X500Name("CN=BankOfCorda,O=R3,L=New York,C=US"), serviceHub.legalIdentityKey)
|
||||
|
||||
val issuance: SignedTransaction = run {
|
||||
val tx = CommercialPaper().generateIssue(party.ref(1, 2, 3), 1100.DOLLARS `issued by` DUMMY_CASH_ISSUER,
|
||||
Instant.now() + 10.days, notaryNode.notaryIdentity)
|
||||
|
||||
// TODO: Consider moving these two steps below into generateIssue.
|
||||
|
||||
// Attach the prospectus.
|
||||
tx.addAttachment(serviceHub.attachments.openAttachment(PROSPECTUS_HASH)!!.id)
|
||||
|
||||
// Requesting a time-window to be set, all CP must have a validation window.
|
||||
tx.setTimeWindow(Instant.now(), 30.seconds)
|
||||
|
||||
// Sign it as ourselves.
|
||||
val stx = serviceHub.signInitialTransaction(tx)
|
||||
|
||||
subFlow(FinalityFlow(stx)).single()
|
||||
}
|
||||
|
||||
// Now make a dummy transaction that moves it to a new key, just to show that resolving dependencies works.
|
||||
val move: SignedTransaction = run {
|
||||
val builder = TransactionBuilder(notaryNode.notaryIdentity)
|
||||
CommercialPaper().generateMove(builder, issuance.tx.outRef(0), ownedBy)
|
||||
val stx = serviceHub.signInitialTransaction(builder)
|
||||
subFlow(FinalityFlow(stx)).single()
|
||||
}
|
||||
|
||||
return move.tx.outRef(0)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,7 +22,7 @@
|
||||
<Logger name="BasicInfo" additivity="false">
|
||||
<AppenderRef ref="Console-Appender-Println"/>
|
||||
</Logger>
|
||||
<Logger name="org.hibernate.SQL" level="debug" additivity="false">
|
||||
<Logger name="org.hibernate.SQL" level="info" additivity="false">
|
||||
<AppenderRef ref="Console-Appender"/>
|
||||
</Logger>
|
||||
</Loggers>
|
||||
|
@ -11,13 +11,15 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.VerifierType
|
||||
import net.corda.node.services.config.configureDevKeyAndTrustStores
|
||||
import net.corda.node.services.identity.InMemoryIdentityService
|
||||
import net.corda.node.utilities.CertificateType
|
||||
import net.corda.node.utilities.X509Utilities
|
||||
import net.corda.nodeapi.config.SSLConfiguration
|
||||
import net.corda.testing.node.MockServices
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
@ -67,9 +69,9 @@ val ALICE_PUBKEY: PublicKey get() = ALICE_KEY.public
|
||||
val BOB_PUBKEY: PublicKey get() = BOB_KEY.public
|
||||
val CHARLIE_PUBKEY: PublicKey get() = CHARLIE_KEY.public
|
||||
|
||||
val MEGA_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(X509Utilities.getX509Name("MegaCorp","London","demo@r3.com",null), MEGA_CORP_PUBKEY)
|
||||
val MEGA_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(getX509Name("MegaCorp", "London", "demo@r3.com", null), MEGA_CORP_PUBKEY)
|
||||
val MEGA_CORP: Party get() = MEGA_CORP_IDENTITY.party
|
||||
val MINI_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(X509Utilities.getX509Name("MiniCorp","London","demo@r3.com",null), MINI_CORP_PUBKEY)
|
||||
val MINI_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(getX509Name("MiniCorp", "London", "demo@r3.com", null), MINI_CORP_PUBKEY)
|
||||
val MINI_CORP: Party get() = MINI_CORP_IDENTITY.party
|
||||
|
||||
val BOC_KEY: KeyPair by lazy { generateKeyPair() }
|
||||
@ -80,7 +82,7 @@ val BOC_PARTY_REF = BOC.ref(OpaqueBytes.of(1)).reference
|
||||
|
||||
val BIG_CORP_KEY: KeyPair by lazy { generateKeyPair() }
|
||||
val BIG_CORP_PUBKEY: PublicKey get() = BIG_CORP_KEY.public
|
||||
val BIG_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(X509Utilities.getX509Name("BigCorporation","London","demo@r3.com",null), BIG_CORP_PUBKEY)
|
||||
val BIG_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(getX509Name("BigCorporation", "London", "demo@r3.com", null), BIG_CORP_PUBKEY)
|
||||
val BIG_CORP: Party get() = BIG_CORP_IDENTITY.party
|
||||
val BIG_CORP_PARTY_REF = BIG_CORP.ref(OpaqueBytes.of(1)).reference
|
||||
|
||||
|
@ -63,6 +63,8 @@ fun initialiseTestSerialization() {
|
||||
(SerializationDefaults.SERIALIZATION_FACTORY as TestSerializationFactory).delegate = SerializationFactoryImpl().apply {
|
||||
registerScheme(KryoClientSerializationScheme())
|
||||
registerScheme(KryoServerSerializationScheme())
|
||||
registerScheme(AMQPClientSerializationScheme())
|
||||
registerScheme(AMQPServerSerializationScheme())
|
||||
}
|
||||
(SerializationDefaults.P2P_CONTEXT as TestSerializationContext).delegate = KRYO_P2P_CONTEXT
|
||||
(SerializationDefaults.RPC_SERVER_CONTEXT as TestSerializationContext).delegate = KRYO_RPC_SERVER_CONTEXT
|
||||
|
@ -4,7 +4,9 @@ package net.corda.testing
|
||||
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.TypeOnlyCommandData
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.CertificateAndKeyPair
|
||||
import net.corda.core.crypto.entropyToKeyPair
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.crypto.testing.DummyPublicKey
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
@ -12,6 +14,7 @@ import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import net.corda.node.utilities.X509Utilities
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.driver.DriverDSLExposedInterface
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
|
@ -1,12 +1,9 @@
|
||||
package net.corda.testing.contracts
|
||||
|
||||
import net.corda.core.contracts.CommandData
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.LinearState
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.contracts.clauses.Clause
|
||||
import net.corda.core.contracts.clauses.FilterOn
|
||||
import net.corda.core.contracts.clauses.verifyClause
|
||||
import net.corda.core.contracts.requireThat
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.containsAny
|
||||
import net.corda.core.identity.AbstractParty
|
||||
@ -22,10 +19,17 @@ import java.time.ZoneOffset.UTC
|
||||
class DummyLinearContract : Contract {
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("Test")
|
||||
|
||||
val clause: Clause<State, CommandData, Unit> = LinearState.ClauseVerifier()
|
||||
override fun verify(tx: LedgerTransaction) = verifyClause(tx,
|
||||
FilterOn(clause, { states -> states.filterIsInstance<State>() }),
|
||||
emptyList())
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
val inputs = tx.inputs.map { it.state.data }.filterIsInstance<State>()
|
||||
val outputs = tx.outputs.map { it.data }.filterIsInstance<State>()
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
data class State(
|
||||
override val linearId: UniqueIdentifier = UniqueIdentifier(),
|
||||
|
@ -13,9 +13,9 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.testing.CHARLIE
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.DUMMY_NOTARY_KEY
|
||||
@ -228,10 +228,11 @@ fun ServiceHub.evolveLinearState(linearState: StateAndRef<LinearState>) : StateA
|
||||
@JvmOverloads
|
||||
fun ServiceHub.consumeCash(amount: Amount<Currency>, to: Party = CHARLIE): Vault.Update<ContractState> {
|
||||
val update = vaultService.rawUpdates.toFuture()
|
||||
val services = this
|
||||
|
||||
// A tx that spends our money.
|
||||
val spendTX = TransactionBuilder(DUMMY_NOTARY).apply {
|
||||
vaultService.generateSpend(this, amount, to)
|
||||
Cash.generateSpend(services, this, amount, to)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}.toSignedTransaction(checkSufficientSignatures = false)
|
||||
|
||||
|
@ -11,12 +11,12 @@ import net.corda.cordform.CordformNode
|
||||
import net.corda.cordform.NodeDefinition
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.concurrent.firstOf
|
||||
import net.corda.core.crypto.X509Utilities
|
||||
import net.corda.core.crypto.appendToCommonName
|
||||
import net.corda.core.crypto.commonName
|
||||
import net.corda.core.crypto.getX509Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.concurrent.*
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.internal.concurrent.*
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.times
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
@ -53,9 +53,12 @@ import java.time.Instant
|
||||
import java.time.ZoneOffset.UTC
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
import java.util.concurrent.*
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
import java.util.concurrent.TimeoutException
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
@ -566,7 +569,7 @@ class DriverDSL(
|
||||
val rpcAddress = portAllocation.nextHostAndPort()
|
||||
val webAddress = portAllocation.nextHostAndPort()
|
||||
// TODO: Derive name from the full picked name, don't just wrap the common name
|
||||
val name = providedName ?: X509Utilities.getX509Name("${oneOf(names).commonName}-${p2pAddress.port}","London","demo@r3.com",null)
|
||||
val name = providedName ?: getX509Name("${oneOf(names).commonName}-${p2pAddress.port}", "London", "demo@r3.com", null)
|
||||
val networkMapServiceConfigLookup = networkMapServiceConfigLookup(listOf(object : NodeDefinition {
|
||||
override fun getName() = name.toString()
|
||||
override fun getConfig() = configOf("p2pAddress" to p2pAddress.toString())
|
||||
|
@ -3,8 +3,8 @@ package net.corda.testing.node
|
||||
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.crypto.getX509Name
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.crypto.X509Utilities
|
||||
import net.corda.core.messaging.AllPossibleRecipients
|
||||
import net.corda.core.messaging.MessageRecipientGroup
|
||||
import net.corda.core.messaging.MessageRecipients
|
||||
@ -128,7 +128,7 @@ class InMemoryMessagingNetwork(
|
||||
id: Int,
|
||||
executor: AffinityExecutor,
|
||||
advertisedServices: List<ServiceEntry>,
|
||||
description: X500Name = X509Utilities.getX509Name("In memory node $id","London","demo@r3.com",null),
|
||||
description: X500Name = getX509Name("In memory node $id", "London", "demo@r3.com", null),
|
||||
database: CordaPersistence)
|
||||
: MessagingServiceBuilder<InMemoryMessaging> {
|
||||
val peerHandle = PeerHandle(id, description)
|
||||
|
@ -9,6 +9,7 @@ import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.*
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
@ -24,11 +25,14 @@ import net.corda.node.services.persistence.InMemoryStateMachineRecordedTransacti
|
||||
import net.corda.node.services.schema.HibernateObserver
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
||||
import net.corda.node.services.vault.HibernateVaultQueryImpl
|
||||
import net.corda.node.services.vault.NodeVaultService
|
||||
import net.corda.testing.DUMMY_CA
|
||||
import net.corda.testing.MEGA_CORP
|
||||
import net.corda.testing.MOCK_IDENTITIES
|
||||
import net.corda.testing.getTestPartyAndCertificate
|
||||
import net.corda.node.utilities.CordaPersistence
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.schemas.CashSchemaV1
|
||||
import net.corda.schemas.CommercialPaperSchemaV1
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.schemas.DummyLinearStateSchemaV1
|
||||
import org.bouncycastle.operator.ContentSigner
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
@ -84,7 +88,7 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub {
|
||||
|
||||
lateinit var hibernatePersister: HibernateObserver
|
||||
|
||||
fun makeVaultService(dataSourceProps: Properties, hibernateConfig: HibernateConfiguration = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties())): VaultService {
|
||||
fun makeVaultService(dataSourceProps: Properties, hibernateConfig: HibernateConfiguration = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties(), identityService)): VaultService {
|
||||
val vaultService = NodeVaultService(this, dataSourceProps, makeTestDatabaseProperties())
|
||||
hibernatePersister = HibernateObserver(vaultService.rawUpdates, hibernateConfig)
|
||||
return vaultService
|
||||
@ -212,4 +216,30 @@ fun makeTestDatabaseProperties(): Properties {
|
||||
return props
|
||||
}
|
||||
|
||||
fun makeTestDatabaseAndMockServices(customSchemas: Set<MappedSchema> = setOf(CommercialPaperSchemaV1, DummyLinearStateSchemaV1, CashSchemaV1), keys: List<KeyPair> = listOf(MEGA_CORP_KEY)): Pair<CordaPersistence, MockServices> {
|
||||
val dataSourceProps = makeTestDataSourceProperties()
|
||||
val databaseProperties = makeTestDatabaseProperties()
|
||||
val database = configureDatabase(dataSourceProps, databaseProperties)
|
||||
val mockService = database.transaction {
|
||||
val identityService = InMemoryIdentityService(MOCK_IDENTITIES, trustRoot = DUMMY_CA.certificate)
|
||||
val hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), databaseProperties, identityService)
|
||||
object : MockServices(*(keys.toTypedArray())) {
|
||||
override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)
|
||||
|
||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||
for (stx in txs) {
|
||||
validatedTransactions.addTransaction(stx)
|
||||
}
|
||||
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
|
||||
vaultService.notifyAll(txs.map { it.tx })
|
||||
}
|
||||
|
||||
override val vaultQueryService: VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
|
||||
|
||||
override fun jdbcSession(): Connection = database.createSession()
|
||||
}
|
||||
}
|
||||
return Pair(database, mockService)
|
||||
}
|
||||
|
||||
val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor")
|
||||
|
@ -1,10 +1,13 @@
|
||||
package net.corda.testing.node
|
||||
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.crypto.X509Utilities
|
||||
import net.corda.core.crypto.appendToCommonName
|
||||
import net.corda.core.crypto.commonName
|
||||
import net.corda.core.internal.concurrent.*
|
||||
import net.corda.core.crypto.getX509Name
|
||||
import net.corda.core.internal.concurrent.flatMap
|
||||
import net.corda.core.internal.concurrent.fork
|
||||
import net.corda.core.internal.concurrent.map
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
@ -120,13 +123,13 @@ abstract class NodeBasedTest : TestDependencyInjectionBase() {
|
||||
val nodeAddresses = getFreeLocalPorts("localhost", clusterSize).map { it.toString() }
|
||||
|
||||
val masterNodeFuture = startNode(
|
||||
X509Utilities.getX509Name("${notaryName.commonName}-0","London","demo@r3.com",null),
|
||||
getX509Name("${notaryName.commonName}-0", "London", "demo@r3.com", null),
|
||||
advertisedServices = setOf(serviceInfo),
|
||||
configOverrides = mapOf("notaryNodeAddress" to nodeAddresses[0]))
|
||||
|
||||
val remainingNodesFutures = (1 until clusterSize).map {
|
||||
startNode(
|
||||
X509Utilities.getX509Name("${notaryName.commonName}-$it","London","demo@r3.com",null),
|
||||
getX509Name("${notaryName.commonName}-$it", "London", "demo@r3.com", null),
|
||||
advertisedServices = setOf(serviceInfo),
|
||||
configOverrides = mapOf(
|
||||
"notaryNodeAddress" to nodeAddresses[it],
|
||||
|
@ -1,6 +1,6 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
import net.corda.core.crypto.X509Utilities.getX509Name
|
||||
import net.corda.core.crypto.getX509Name
|
||||
import net.corda.demobench.plugin.PluginController
|
||||
import net.corda.demobench.pty.R3Pty
|
||||
import tornadofx.*
|
||||
|
@ -1,8 +1,8 @@
|
||||
package net.corda.demobench.model
|
||||
|
||||
import net.corda.core.crypto.X509Utilities.getX509Name
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.core.crypto.getX509Name
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import org.junit.Test
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
@ -3,7 +3,6 @@ package net.corda.verifier
|
||||
import net.corda.client.mock.*
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.X509Utilities
|
||||
import net.corda.core.crypto.entropyToKeyPair
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.identity.AbstractParty
|
||||
@ -12,6 +11,7 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.getTestX509Name
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.math.BigInteger
|
||||
import java.security.PublicKey
|
||||
@ -179,7 +179,7 @@ fun commandGenerator(partiesToPickFrom: Collection<Party>): Generator<Pair<Comma
|
||||
}
|
||||
|
||||
val partyGenerator: Generator<Party> = Generator.int().combine(publicKeyGenerator) { n, key ->
|
||||
Party(X509Utilities.getDevX509Name("Party$n"), key)
|
||||
Party(getTestX509Name("Party$n"), key)
|
||||
}
|
||||
|
||||
fun <A> pickOneOrMaybeNew(from: Collection<A>, generator: Generator<A>): Generator<A> {
|
||||
|
@ -2,15 +2,13 @@ package net.corda.verifier
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import net.corda.core.concurrent.*
|
||||
import net.corda.core.crypto.X509Utilities
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.crypto.commonName
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.crypto.random63BitValue
|
||||
import net.corda.core.internal.concurrent.*
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.testing.driver.ProcessUtilities
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.config.configureDevKeyAndTrustStores
|
||||
import net.corda.nodeapi.ArtemisMessagingComponent.Companion.NODE_USER
|
||||
@ -20,6 +18,7 @@ import net.corda.nodeapi.VerifierApi
|
||||
import net.corda.nodeapi.config.NodeSSLConfiguration
|
||||
import net.corda.nodeapi.config.SSLConfiguration
|
||||
import net.corda.testing.driver.*
|
||||
import net.corda.testing.getTestX509Name
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.api.core.client.ActiveMQClient
|
||||
import org.apache.activemq.artemis.api.core.client.ClientProducer
|
||||
@ -250,7 +249,7 @@ data class VerifierDriverDSL(
|
||||
val id = verifierCount.andIncrement
|
||||
val jdwpPort = if (driverDSL.isDebug) driverDSL.debugPortAllocation.nextPort() else null
|
||||
val processFuture = driverDSL.executorService.fork {
|
||||
val verifierName = X509Utilities.getDevX509Name("verifier$id")
|
||||
val verifierName = getTestX509Name("verifier$id")
|
||||
val baseDirectory = driverDSL.driverDirectory / verifierName.commonName
|
||||
val config = createConfiguration(baseDirectory, address)
|
||||
val configFilename = "verifier.conf"
|
||||
|
Loading…
Reference in New Issue
Block a user