mirror of
https://github.com/corda/corda.git
synced 2025-06-16 14:18:20 +00:00
Merge open master
This commit is contained in:
@ -1,9 +1,9 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.composite.CompositeKey
|
||||
import net.corda.core.utilities.exactAdd
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.exactAdd
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.util.*
|
||||
@ -13,6 +13,7 @@ import java.util.*
|
||||
* indicative/displayed asset amounts in [BigDecimal] to fungible tokens represented by Amount objects.
|
||||
*/
|
||||
interface TokenizableAssetInfo {
|
||||
/** The nominal display unit size of a single token, potentially with trailing decimal display places if the scale parameter is non-zero. */
|
||||
val displayTokenSize: BigDecimal
|
||||
}
|
||||
|
||||
@ -28,16 +29,14 @@ interface TokenizableAssetInfo {
|
||||
* multiplication are overflow checked and will throw [ArithmeticException] if the operation would have caused integer
|
||||
* overflow.
|
||||
*
|
||||
* @param quantity the number of tokens as a Long value.
|
||||
* @param displayTokenSize the nominal display unit size of a single token,
|
||||
* potentially with trailing decimal display places if the scale parameter is non-zero.
|
||||
* @param T the type of the token, for example [Currency].
|
||||
* T should implement TokenizableAssetInfo if automatic conversion to/from a display format is required.
|
||||
*
|
||||
* TODO Proper lookup of currencies in a locale and context sensitive fashion is not supported and is left to the application.
|
||||
* @property quantity the number of tokens as a Long value.
|
||||
* @property displayTokenSize the nominal display unit size of a single token, potentially with trailing decimal display places if the scale parameter is non-zero.
|
||||
* @property token an instance of type T, usually a singleton.
|
||||
* @param T the type of the token, for example [Currency]. T should implement TokenizableAssetInfo if automatic conversion to/from a display format is required.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal, val token: T) : Comparable<Amount<T>> {
|
||||
// TODO Proper lookup of currencies in a locale and context sensitive fashion is not supported and is left to the application.
|
||||
companion object {
|
||||
/**
|
||||
* Build an Amount from a decimal representation. For example, with an input of "12.34 GBP",
|
||||
@ -73,6 +72,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
* For other possible token types the asset token should implement TokenizableAssetInfo to
|
||||
* correctly report the designed nominal amount.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getDisplayTokenSize(token: Any): BigDecimal {
|
||||
if (token is TokenizableAssetInfo) {
|
||||
return token.displayTokenSize
|
||||
@ -86,14 +86,39 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
return BigDecimal.ONE
|
||||
}
|
||||
|
||||
/**
|
||||
* If the given iterable of [Amount]s yields any elements, sum them, throwing an [IllegalArgumentException] if
|
||||
* any of the token types are mismatched; if the iterator yields no elements, return null.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun <T : Any> Iterable<Amount<T>>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow()
|
||||
|
||||
/**
|
||||
* Sums the amounts yielded by the given iterable, throwing an [IllegalArgumentException] if any of the token
|
||||
* types are mismatched.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun <T : Any> Iterable<Amount<T>>.sumOrThrow() = reduce { left, right -> left + right }
|
||||
|
||||
/**
|
||||
* If the given iterable of [Amount]s yields any elements, sum them, throwing an [IllegalArgumentException] if
|
||||
* any of the token types are mismatched; if the iterator yields no elements, return a zero amount of the given
|
||||
* token type.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun <T : Any> Iterable<Amount<T>>.sumOrZero(token: T) = if (iterator().hasNext()) sumOrThrow() else Amount.zero(token)
|
||||
|
||||
private val currencySymbols: Map<String, Currency> = mapOf(
|
||||
"$" to USD,
|
||||
"£" to GBP,
|
||||
"€" to EUR,
|
||||
"¥" to JPY,
|
||||
"₽" to RUB
|
||||
"$" to Currency.getInstance("USD"),
|
||||
"£" to Currency.getInstance("GBP"),
|
||||
"€" to Currency.getInstance("EUR"),
|
||||
"¥" to Currency.getInstance("JPY"),
|
||||
"₽" to Currency.getInstance("RUB")
|
||||
)
|
||||
private val currencyCodes: Map<String, Currency> by lazy { Currency.getAvailableCurrencies().map { it.currencyCode to it }.toMap() }
|
||||
|
||||
private val currencyCodes: Map<String, Currency> by lazy {
|
||||
Currency.getAvailableCurrencies().associateBy { it.currencyCode }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an amount that is equal to the given currency amount in text. Examples of what is supported:
|
||||
@ -120,6 +145,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
*
|
||||
* @throws IllegalArgumentException if the input string was not understood.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun parseCurrency(input: String): Amount<Currency> {
|
||||
val i = input.filter { it != ',' }
|
||||
try {
|
||||
@ -127,7 +153,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
for ((symbol, currency) in currencySymbols) {
|
||||
if (i.startsWith(symbol)) {
|
||||
val rest = i.substring(symbol.length)
|
||||
return fromDecimal(BigDecimal(rest), currency)
|
||||
return Amount.fromDecimal(BigDecimal(rest), currency)
|
||||
}
|
||||
}
|
||||
// Now check the codes at the end.
|
||||
@ -136,7 +162,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
val (rest, code) = split
|
||||
for ((cc, currency) in currencyCodes) {
|
||||
if (cc == code) {
|
||||
return fromDecimal(BigDecimal(rest), currency)
|
||||
return Amount.fromDecimal(BigDecimal(rest), currency)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -166,8 +192,9 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
|
||||
/**
|
||||
* A checked addition operator is supported to simplify aggregation of Amounts.
|
||||
* Mixing non-identical token types will throw [IllegalArgumentException].
|
||||
*
|
||||
* @throws ArithmeticException if there is overflow of Amount tokens during the summation
|
||||
* Mixing non-identical token types will throw [IllegalArgumentException]
|
||||
*/
|
||||
operator fun plus(other: Amount<T>): Amount<T> {
|
||||
checkToken(other)
|
||||
@ -177,8 +204,9 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
/**
|
||||
* A checked addition operator is supported to simplify netting of Amounts.
|
||||
* If this leads to the Amount going negative this will throw [IllegalArgumentException].
|
||||
* Mixing non-identical token types will throw [IllegalArgumentException].
|
||||
*
|
||||
* @throws ArithmeticException if there is Numeric underflow
|
||||
* Mixing non-identical token types will throw [IllegalArgumentException]
|
||||
*/
|
||||
operator fun minus(other: Amount<T>): Amount<T> {
|
||||
checkToken(other)
|
||||
@ -197,6 +225,11 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
*/
|
||||
operator fun times(other: Long): Amount<T> = Amount(Math.multiplyExact(quantity, other), displayTokenSize, token)
|
||||
|
||||
/**
|
||||
* The multiplication operator is supported to allow easy calculation for multiples of a primitive Amount.
|
||||
* Note this is not a conserving operation, so it may not always be correct modelling of proper token behaviour.
|
||||
* N.B. Division is not supported as fractional tokens are not representable by an Amount.
|
||||
*/
|
||||
operator fun times(other: Int): Amount<T> = Amount(Math.multiplyExact(quantity, other.toLong()), displayTokenSize, token)
|
||||
|
||||
/**
|
||||
@ -219,7 +252,7 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
* of "1234" GBP, returns "12.34". The precise representation is controlled by the displayTokenSize,
|
||||
* which determines the size of a single token and controls the trailing decimal places via it's scale property.
|
||||
*
|
||||
* @see Amount.Companion.fromDecimal
|
||||
* @see Amount.fromDecimal
|
||||
*/
|
||||
fun toDecimal(): BigDecimal = BigDecimal.valueOf(quantity, 0) * displayTokenSize
|
||||
|
||||
@ -231,29 +264,27 @@ data class Amount<T : Any>(val quantity: Long, val displayTokenSize: BigDecimal,
|
||||
* The result of fromDecimal is used to control the numerical formatting and
|
||||
* the token specifier appended is taken from token.toString.
|
||||
*
|
||||
* @see Amount.Companion.fromDecimal
|
||||
* @see Amount.fromDecimal
|
||||
*/
|
||||
override fun toString(): String {
|
||||
return toDecimal().toPlainString() + " " + token
|
||||
}
|
||||
|
||||
/** @suppress */
|
||||
override fun compareTo(other: Amount<T>): Int {
|
||||
checkToken(other)
|
||||
return quantity.compareTo(other.quantity)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun <T : Any> Iterable<Amount<T>>.sumOrNull() = if (!iterator().hasNext()) null else sumOrThrow()
|
||||
fun <T : Any> Iterable<Amount<T>>.sumOrThrow() = reduce { left, right -> left + right }
|
||||
fun <T : Any> Iterable<Amount<T>>.sumOrZero(token: T) = if (iterator().hasNext()) sumOrThrow() else Amount.zero(token)
|
||||
|
||||
|
||||
/**
|
||||
* Simple data class to associate the origin, owner, or holder of a particular Amount object.
|
||||
* @param source the holder of the Amount.
|
||||
* @param amount the Amount of asset available.
|
||||
* @param ref is an optional field used for housekeeping in the caller.
|
||||
*
|
||||
* @param P Any class type that can disambiguate where the amount came from.
|
||||
* @param T The token type of the underlying [Amount].
|
||||
* @property source the holder of the Amount.
|
||||
* @property amount the Amount of asset available.
|
||||
* @property ref is an optional field used for housekeeping in the caller.
|
||||
* e.g. to point back at the original Vault state objects.
|
||||
* @see SourceAndAmount.apply which processes a list of SourceAndAmount objects
|
||||
* and calculates the resulting Amount distribution as a new list of SourceAndAmount objects.
|
||||
@ -263,17 +294,17 @@ data class SourceAndAmount<T : Any, out P : Any>(val source: P, val amount: Amou
|
||||
/**
|
||||
* This class represents a possibly negative transfer of tokens from one vault state to another, possibly at a future date.
|
||||
*
|
||||
* @param quantityDelta is a signed Long value representing the exchanged number of tokens. If positive then
|
||||
* @property quantityDelta is a signed Long value representing the exchanged number of tokens. If positive then
|
||||
* it represents the movement of Math.abs(quantityDelta) tokens away from source and receipt of Math.abs(quantityDelta)
|
||||
* at the destination. If the quantityDelta is negative then the source will receive Math.abs(quantityDelta) tokens
|
||||
* and the destination will lose Math.abs(quantityDelta) tokens.
|
||||
* Where possible the source and destination should be coded to ensure a positive quantityDelta,
|
||||
* but in various scenarios it may be more consistent to allow positive and negative values.
|
||||
* For example it is common for a bank to code asset flows as gains and losses from its perspective i.e. always the destination.
|
||||
* @param token represents the type of asset token as would be used to construct Amount<T> objects.
|
||||
* @param source is the [Party], [CompositeKey], or other identifier of the token source if quantityDelta is positive,
|
||||
* @property token represents the type of asset token as would be used to construct Amount<T> objects.
|
||||
* @property source is the [Party], [CompositeKey], or other identifier of the token source if quantityDelta is positive,
|
||||
* or the token sink if quantityDelta is negative. The type P should support value equality.
|
||||
* @param destination is the [Party], [CompositeKey], or other identifier of the token sink if quantityDelta is positive,
|
||||
* @property destination is the [Party], [CompositeKey], or other identifier of the token sink if quantityDelta is positive,
|
||||
* or the token source if quantityDelta is negative. The type P should support value equality.
|
||||
*/
|
||||
@CordaSerializable
|
||||
@ -305,9 +336,7 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
|
||||
return AmountTransfer(deltaTokenCount, token, source, destination)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to make a zero size AmountTransfer
|
||||
*/
|
||||
/** Helper to make a zero size AmountTransfer. */
|
||||
@JvmStatic
|
||||
fun <T : Any, P : Any> zero(token: T,
|
||||
source: P,
|
||||
@ -344,6 +373,7 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
|
||||
*/
|
||||
fun toDecimal(): BigDecimal = BigDecimal.valueOf(quantityDelta, 0) * Amount.getDisplayTokenSize(token)
|
||||
|
||||
/** @suppress */
|
||||
fun copy(quantityDelta: Long = this.quantityDelta,
|
||||
token: T = this.token,
|
||||
source: P = this.source,
|
||||
@ -373,7 +403,7 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
|
||||
}
|
||||
|
||||
/**
|
||||
* HashCode ensures that reversed source and destination equivalents will hash to the same value.
|
||||
* This hash code function ensures that reversed source and destination equivalents will hash to the same value.
|
||||
*/
|
||||
override fun hashCode(): Int {
|
||||
var result = Math.abs(quantityDelta).hashCode() // ignore polarity reversed values
|
||||
@ -382,18 +412,20 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
|
||||
return result
|
||||
}
|
||||
|
||||
/** @suppress */
|
||||
override fun toString(): String {
|
||||
return "Transfer from $source to $destination of ${this.toDecimal().toPlainString()} $token"
|
||||
}
|
||||
|
||||
/**
|
||||
* Novation is a common financial operation in which a bilateral exchange is modified so that the same
|
||||
* relative asset exchange happens, but with each party exchanging versus a central counterparty, or clearing house.
|
||||
* Returns a list of two new AmountTransfers each between one of the original parties and the centralParty. The net
|
||||
* total exchange is the same as in the original input. Novation is a common financial operation in which a
|
||||
* bilateral exchange is modified so that the same relative asset exchange happens, but with each party exchanging
|
||||
* versus a central counterparty, or clearing house.
|
||||
*
|
||||
* @param centralParty The central party to face the exchange against.
|
||||
* @return Returns a list of two new AmountTransfers each between one of the original parties and the centralParty.
|
||||
* The net total exchange is the same as in the original input.
|
||||
*/
|
||||
@Suppress("UNUSED")
|
||||
fun novate(centralParty: P): List<AmountTransfer<T, P>> = listOf(copy(destination = centralParty), copy(source = centralParty))
|
||||
|
||||
/**
|
||||
@ -403,7 +435,7 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
|
||||
* @param balances The source list of [SourceAndAmount] objects containing the funds to satisfy the exchange.
|
||||
* @param newRef An optional marker object which is attached to any new [SourceAndAmount] objects created in the output.
|
||||
* i.e. To the new payment destination entry and to any residual change output.
|
||||
* @return The returned list is a copy of the original list, except that funds needed to cover the exchange
|
||||
* @return A copy of the original list, except that funds needed to cover the exchange
|
||||
* will have been removed and a new output and possibly residual amount entry will be added at the end of the list.
|
||||
* @throws ArithmeticException if there is underflow in the summations.
|
||||
*/
|
||||
|
@ -4,7 +4,6 @@ package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import java.math.BigDecimal
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
@ -12,41 +11,10 @@ import java.util.*
|
||||
* Defines a simple domain specific language for the specification of financial contracts. Currently covers:
|
||||
*
|
||||
* - Some utilities for working with commands.
|
||||
* - Code for working with currencies.
|
||||
* - An Amount type that represents a positive quantity of a specific currency.
|
||||
* - An Amount type that represents a positive quantity of a specific token.
|
||||
* - A simple language extension for specifying requirements in English, along with logic to enforce them.
|
||||
*
|
||||
* TODO: Look into replacing Currency and Amount with CurrencyUnit and MonetaryAmount from the javax.money API (JSR 354)
|
||||
*/
|
||||
|
||||
//// Currencies ///////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
fun currency(code: String) = Currency.getInstance(code)!!
|
||||
|
||||
@JvmField val USD = currency("USD")
|
||||
@JvmField val GBP = currency("GBP")
|
||||
@JvmField val EUR = currency("EUR")
|
||||
@JvmField val CHF = currency("CHF")
|
||||
@JvmField val JPY = currency("JPY")
|
||||
@JvmField val RUB = currency("RUB")
|
||||
|
||||
fun <T : Any> AMOUNT(amount: Int, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token)
|
||||
fun <T : Any> AMOUNT(amount: Double, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount), token)
|
||||
fun DOLLARS(amount: Int): Amount<Currency> = AMOUNT(amount, USD)
|
||||
fun DOLLARS(amount: Double): Amount<Currency> = AMOUNT(amount, USD)
|
||||
fun POUNDS(amount: Int): Amount<Currency> = AMOUNT(amount, GBP)
|
||||
fun SWISS_FRANCS(amount: Int): Amount<Currency> = AMOUNT(amount, CHF)
|
||||
|
||||
val Int.DOLLARS: Amount<Currency> get() = DOLLARS(this)
|
||||
val Double.DOLLARS: Amount<Currency> get() = DOLLARS(this)
|
||||
val Int.POUNDS: Amount<Currency> get() = POUNDS(this)
|
||||
val Int.SWISS_FRANCS: Amount<Currency> get() = SWISS_FRANCS(this)
|
||||
|
||||
infix fun Currency.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
|
||||
infix fun Amount<Currency>.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
|
||||
infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this)
|
||||
infix fun Amount<Currency>.issuedBy(deposit: PartyAndReference) = Amount(quantity, displayTokenSize, token.issuedBy(deposit))
|
||||
|
||||
//// Requirements /////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
object Requirements {
|
||||
|
@ -14,49 +14,28 @@ class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException
|
||||
* crude are fungible and countable (oil from two small containers can be poured into one large
|
||||
* container), shares of the same class in a specific company are fungible and countable, and so on.
|
||||
*
|
||||
* See [Cash] for an example contract that implements currency using state objects that implement
|
||||
* An example usage would be a cash transaction contract that implements currency using state objects that implement
|
||||
* this interface.
|
||||
*
|
||||
* @param T a type that represents the asset in question. This should describe the basic type of the asset
|
||||
* (GBP, USD, oil, shares in company <X>, etc.) and any additional metadata (issuer, grade, class, etc.).
|
||||
*/
|
||||
interface FungibleAsset<T : Any> : OwnableState {
|
||||
/**
|
||||
* Amount represents a positive quantity of some issued product which can be cash, tokens, assets, or generally
|
||||
* anything else that's quantifiable with integer quantities. See [Issued] and [Amount] for more details.
|
||||
*/
|
||||
val amount: Amount<Issued<T>>
|
||||
|
||||
/**
|
||||
* There must be an ExitCommand signed by these keys to destroy the amount. While all states require their
|
||||
* owner to sign, some (i.e. cash) also require the issuer.
|
||||
*/
|
||||
val exitKeys: Collection<PublicKey>
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
override val owner: AbstractParty
|
||||
|
||||
fun move(newAmount: Amount<Issued<T>>, newOwner: AbstractParty): FungibleAsset<T>
|
||||
|
||||
// Just for grouping
|
||||
interface Commands : CommandData {
|
||||
interface Move : MoveCommand, Commands
|
||||
|
||||
/**
|
||||
* Allows new asset states to be issued into existence: the nonce ("number used once") ensures the transaction
|
||||
* has a unique ID even when there are no inputs.
|
||||
*/
|
||||
interface Issue : IssueCommand, Commands
|
||||
|
||||
/**
|
||||
* A command stating that money has been withdrawn from the shared ledger and is now accounted for
|
||||
* in some other way.
|
||||
*/
|
||||
interface Exit<T : Any> : Commands {
|
||||
val amount: Amount<Issued<T>>
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Copies the underlying data structure, replacing the amount and owner fields with the new values and leaving the
|
||||
* rest (exitKeys) alone.
|
||||
*/
|
||||
fun withNewOwnerAndAmount(newAmount: Amount<Issued<T>>, newOwner: AbstractParty): FungibleAsset<T>
|
||||
}
|
||||
|
||||
// Small DSL extensions.
|
||||
|
||||
/** Sums the asset states in the list, returning null if there are none. */
|
||||
fun <T : Any> Iterable<ContractState>.sumFungibleOrNull() = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrNull()
|
||||
|
||||
/** Sums the asset states in the list, returning zero of the given token if there are none. */
|
||||
fun <T : Any> Iterable<ContractState>.sumFungibleOrZero(token: Issued<T>) = filterIsInstance<FungibleAsset<T>>().map { it.amount }.sumOrZero(token)
|
||||
|
||||
|
@ -128,10 +128,24 @@ infix fun <T : ContractState> T.`with notary`(newNotary: Party) = withNotary(new
|
||||
infix fun <T : ContractState> T.withNotary(newNotary: Party) = TransactionState(this, newNotary)
|
||||
|
||||
/**
|
||||
* Definition for an issued product, which can be cash, a cash-like thing, assets, or generally anything else that's
|
||||
* quantifiable with integer quantities.
|
||||
* The [Issued] data class holds the details of an on ledger digital asset.
|
||||
* In particular it gives the public credentials of the entity that created these digital tokens
|
||||
* and the particular product represented.
|
||||
*
|
||||
* @param P the type of product underlying the definition, for example [java.util.Currency].
|
||||
* @param P the class type of product underlying the definition, for example [java.util.Currency].
|
||||
* @property issuer The [AbstractParty] details of the entity which issued the asset
|
||||
* and a reference blob, which can contain other details related to the token creation e.g. serial number,
|
||||
* warehouse location, etc.
|
||||
* The issuer is the gatekeeper for creating, or destroying the tokens on the digital ledger and
|
||||
* only their [PrivateKey] signature can authorise transactions that do not conserve the total number
|
||||
* of tokens on the ledger.
|
||||
* Other identities may own the tokens, but they can only create transactions that conserve the total token count.
|
||||
* Typically the issuer is also a well know organisation that can convert digital tokens to external assets
|
||||
* and thus underwrites the digital tokens.
|
||||
* Different issuer values may coexist for a particular product, but these cannot be merged.
|
||||
* @property product The details of the specific product represented by these digital tokens. The value
|
||||
* of product may differentiate different kinds of asset within the same logical class e.g the currency, or
|
||||
* it may just be a type marker for a single custom asset.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Issued<out P : Any>(val issuer: PartyAndReference, val product: P) {
|
||||
@ -156,15 +170,15 @@ data class CommandAndState(val command: CommandData, val ownableState: OwnableSt
|
||||
* A contract state that can have a single owner.
|
||||
*/
|
||||
interface OwnableState : ContractState {
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
/** There must be a MoveCommand signed by this key to claim the amount. */
|
||||
val owner: AbstractParty
|
||||
|
||||
/** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone */
|
||||
/** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone. */
|
||||
fun withNewOwner(newOwner: AbstractParty): CommandAndState
|
||||
}
|
||||
// DOCEND 3
|
||||
|
||||
/** Something which is scheduled to happen at a point in time */
|
||||
/** Something which is scheduled to happen at a point in time. */
|
||||
interface Scheduled {
|
||||
val scheduledAt: Instant
|
||||
}
|
||||
@ -284,20 +298,14 @@ data class Command<T : CommandData>(val value: T, val signers: List<PublicKey>)
|
||||
override fun toString() = "${commandDataToString()} with pubkeys ${signers.joinToString()}"
|
||||
}
|
||||
|
||||
/** A common issue command, to enforce that issue commands have a nonce value. */
|
||||
// TODO: Revisit use of nonce values - should this be part of the TX rather than the command perhaps?
|
||||
interface IssueCommand : CommandData {
|
||||
val nonce: Long
|
||||
}
|
||||
|
||||
/** A common move command for contract states which can change owner. */
|
||||
interface MoveCommand : CommandData {
|
||||
/**
|
||||
* Contract code the moved state(s) are for the attention of, for example to indicate that the states are moved in
|
||||
* order to settle an obligation contract's state object(s).
|
||||
*/
|
||||
// TODO: Replace SecureHash here with a general contract constraints object
|
||||
val contractHash: SecureHash?
|
||||
// TODO: Replace Class here with a general contract constraints object
|
||||
val contract: Class<out Contract>?
|
||||
}
|
||||
|
||||
/** Indicates that this transaction replaces the inputs contract state to another contract state */
|
||||
@ -333,15 +341,14 @@ interface Contract {
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun verify(tx: LedgerTransaction)
|
||||
|
||||
/**
|
||||
* Unparsed reference to the natural language contract that this code is supposed to express (usually a hash of
|
||||
* the contract's contents).
|
||||
*/
|
||||
val legalContractReference: SecureHash
|
||||
}
|
||||
// DOCEND 5
|
||||
|
||||
/** The annotated [Contract] implements the legal prose identified by the given URI. */
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@MustBeDocumented
|
||||
annotation class LegalProseReference(val uri: String)
|
||||
|
||||
/**
|
||||
* Interface which can upgrade state objects issued by a contract to a new state object issued by a different contract.
|
||||
*
|
||||
@ -427,7 +434,7 @@ fun JarInputStream.extractFile(path: String, outputTo: OutputStream) {
|
||||
* A privacy salt is required to compute nonces per transaction component in order to ensure that an adversary cannot
|
||||
* use brute force techniques and reveal the content of a Merkle-leaf hashed value.
|
||||
* Because this salt serves the role of the seed to compute nonces, its size and entropy should be equal to the
|
||||
* underlying hash function used for Merkle tree generation, currently [SHA256], which has an output of 32 bytes.
|
||||
* underlying hash function used for Merkle tree generation, currently [SecureHash.SHA256], which has an output of 32 bytes.
|
||||
* There are two constructors, one that generates a new 32-bytes random salt, and another that takes a [ByteArray] input.
|
||||
* The latter is required in cases where the salt value needs to be pre-generated (agreed between transacting parties),
|
||||
* but it is highlighted that one should always ensure it has sufficient entropy.
|
||||
|
@ -13,7 +13,7 @@ import java.security.Signature
|
||||
* This builder will use bouncy castle's JcaContentSignerBuilder as fallback for unknown algorithm.
|
||||
*/
|
||||
object ContentSignerBuilder {
|
||||
fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider?, random: SecureRandom? = null): ContentSigner {
|
||||
fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider, random: SecureRandom? = null): ContentSigner {
|
||||
val sigAlgId = signatureScheme.signatureOID
|
||||
val sig = Signature.getInstance(signatureScheme.signatureName, provider).apply {
|
||||
if (random != null) {
|
||||
|
@ -70,6 +70,7 @@ object Crypto {
|
||||
* RSA_SHA256 signature scheme using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function.
|
||||
* Note: Recommended key size >= 3072 bits.
|
||||
*/
|
||||
@JvmField
|
||||
val RSA_SHA256 = SignatureScheme(
|
||||
1,
|
||||
"RSA_SHA256",
|
||||
@ -84,6 +85,7 @@ object Crypto {
|
||||
)
|
||||
|
||||
/** ECDSA signature scheme using the secp256k1 Koblitz curve. */
|
||||
@JvmField
|
||||
val ECDSA_SECP256K1_SHA256 = SignatureScheme(
|
||||
2,
|
||||
"ECDSA_SECP256K1_SHA256",
|
||||
@ -98,6 +100,7 @@ object Crypto {
|
||||
)
|
||||
|
||||
/** ECDSA signature scheme using the secp256r1 (NIST P-256) curve. */
|
||||
@JvmField
|
||||
val ECDSA_SECP256R1_SHA256 = SignatureScheme(
|
||||
3,
|
||||
"ECDSA_SECP256R1_SHA256",
|
||||
@ -112,6 +115,7 @@ object Crypto {
|
||||
)
|
||||
|
||||
/** EdDSA signature scheme using the ed255519 twisted Edwards curve. */
|
||||
@JvmField
|
||||
val EDDSA_ED25519_SHA512 = SignatureScheme(
|
||||
4,
|
||||
"EDDSA_ED25519_SHA512",
|
||||
@ -131,7 +135,10 @@ object Crypto {
|
||||
* SPHINCS-256 hash-based signature scheme. It provides 128bit security against post-quantum attackers
|
||||
* at the cost of larger key sizes and loss of compatibility.
|
||||
*/
|
||||
@JvmField
|
||||
val SHA512_256 = DLSequence(arrayOf(NISTObjectIdentifiers.id_sha512_256))
|
||||
|
||||
@JvmField
|
||||
val SPHINCS256_SHA256 = SignatureScheme(
|
||||
5,
|
||||
"SPHINCS-256_SHA512",
|
||||
@ -146,13 +153,12 @@ object Crypto {
|
||||
"at the cost of larger key sizes and loss of compatibility."
|
||||
)
|
||||
|
||||
/**
|
||||
* Corda composite key type
|
||||
*/
|
||||
/** Corda composite key type */
|
||||
@JvmField
|
||||
val COMPOSITE_KEY = SignatureScheme(
|
||||
6,
|
||||
"COMPOSITE",
|
||||
AlgorithmIdentifier(CordaObjectIdentifier.compositeKey),
|
||||
AlgorithmIdentifier(CordaObjectIdentifier.COMPOSITE_KEY),
|
||||
emptyList(),
|
||||
CordaSecurityProvider.PROVIDER_NAME,
|
||||
CompositeKey.KEY_ALGORITHM,
|
||||
@ -163,13 +169,14 @@ object Crypto {
|
||||
)
|
||||
|
||||
/** Our default signature scheme if no algorithm is specified (e.g. for key generation). */
|
||||
@JvmField
|
||||
val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512
|
||||
|
||||
/**
|
||||
* Supported digital signature schemes.
|
||||
* Note: Only the schemes added in this map will be supported (see [Crypto]).
|
||||
*/
|
||||
val supportedSignatureSchemes = listOf(
|
||||
private val signatureSchemeMap: Map<String, SignatureScheme> = listOf(
|
||||
RSA_SHA256,
|
||||
ECDSA_SECP256K1_SHA256,
|
||||
ECDSA_SECP256R1_SHA256,
|
||||
@ -183,15 +190,15 @@ object Crypto {
|
||||
* algorithm identifiers.
|
||||
*/
|
||||
private val algorithmMap: Map<AlgorithmIdentifier, SignatureScheme>
|
||||
= (supportedSignatureSchemes.values.flatMap { scheme -> scheme.alternativeOIDs.map { oid -> Pair(oid, scheme) } }
|
||||
+ supportedSignatureSchemes.values.map { Pair(it.signatureOID, it) })
|
||||
= (signatureSchemeMap.values.flatMap { scheme -> scheme.alternativeOIDs.map { Pair(it, scheme) } }
|
||||
+ signatureSchemeMap.values.map { Pair(it.signatureOID, it) })
|
||||
.toMap()
|
||||
|
||||
// This map is required to defend against users that forcibly call Security.addProvider / Security.removeProvider
|
||||
// that could cause unexpected and suspicious behaviour.
|
||||
// i.e. if someone removes a Provider and then he/she adds a new one with the same name.
|
||||
// The val is private to avoid any harmful state changes.
|
||||
val providerMap: Map<String, Provider> = mapOf(
|
||||
private val providerMap: Map<String, Provider> = mapOf(
|
||||
BouncyCastleProvider.PROVIDER_NAME to getBouncyCastleProvider(),
|
||||
CordaSecurityProvider.PROVIDER_NAME to CordaSecurityProvider(),
|
||||
"BCPQC" to BouncyCastlePQCProvider()) // unfortunately, provider's name is not final in BouncyCastlePQCProvider, so we explicitly set it.
|
||||
@ -201,6 +208,14 @@ object Crypto {
|
||||
addKeyInfoConverter(EDDSA_ED25519_SHA512.signatureOID.algorithm, KeyInfoConverter(EDDSA_ED25519_SHA512))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun supportedSignatureSchemes(): List<SignatureScheme> = ArrayList(signatureSchemeMap.values)
|
||||
|
||||
@JvmStatic
|
||||
fun findProvider(name: String): Provider {
|
||||
return providerMap[name] ?: throw IllegalArgumentException("Unrecognised provider: $name")
|
||||
}
|
||||
|
||||
init {
|
||||
// This registration is needed for reading back EdDSA key from java keystore.
|
||||
// TODO: Find a way to make JKS work with bouncy castle provider or implement our own provide so we don't have to register bouncy castle provider.
|
||||
@ -219,8 +234,10 @@ object Crypto {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun findSignatureScheme(algorithm: AlgorithmIdentifier): SignatureScheme {
|
||||
return algorithmMap[normaliseAlgorithmIdentifier(algorithm)] ?: throw IllegalArgumentException("Unrecognised algorithm: ${algorithm.algorithm.id}")
|
||||
return algorithmMap[normaliseAlgorithmIdentifier(algorithm)]
|
||||
?: throw IllegalArgumentException("Unrecognised algorithm: ${algorithm.algorithm.id}")
|
||||
}
|
||||
|
||||
/**
|
||||
@ -231,8 +248,11 @@ object Crypto {
|
||||
* @return a currently supported SignatureScheme.
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $schemeCodeName")
|
||||
@JvmStatic
|
||||
fun findSignatureScheme(schemeCodeName: String): SignatureScheme {
|
||||
return signatureSchemeMap[schemeCodeName]
|
||||
?: throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $schemeCodeName")
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the corresponding [SignatureScheme] based on the type of the input [Key].
|
||||
@ -242,7 +262,7 @@ object Crypto {
|
||||
* @return a currently supported SignatureScheme.
|
||||
* @throws IllegalArgumentException if the requested key type is not supported.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@JvmStatic
|
||||
fun findSignatureScheme(key: PublicKey): SignatureScheme {
|
||||
val keyInfo = SubjectPublicKeyInfo.getInstance(key.encoded)
|
||||
return findSignatureScheme(keyInfo.algorithm)
|
||||
@ -256,7 +276,7 @@ object Crypto {
|
||||
* @return a currently supported SignatureScheme.
|
||||
* @throws IllegalArgumentException if the requested key type is not supported.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@JvmStatic
|
||||
fun findSignatureScheme(key: PrivateKey): SignatureScheme {
|
||||
val keyInfo = PrivateKeyInfo.getInstance(key.encoded)
|
||||
return findSignatureScheme(keyInfo.privateKeyAlgorithm)
|
||||
@ -269,11 +289,12 @@ object Crypto {
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for this key factory to produce a private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@JvmStatic
|
||||
fun decodePrivateKey(encodedKey: ByteArray): PrivateKey {
|
||||
val keyInfo = PrivateKeyInfo.getInstance(encodedKey)
|
||||
val signatureScheme = findSignatureScheme(keyInfo.privateKeyAlgorithm)
|
||||
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
|
||||
val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
|
||||
return keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -284,8 +305,11 @@ object Crypto {
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for this key factory to produce a private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
|
||||
fun decodePrivateKey(schemeCodeName: String, encodedKey: ByteArray): PrivateKey = decodePrivateKey(findSignatureScheme(schemeCodeName), encodedKey)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeySpecException::class)
|
||||
fun decodePrivateKey(schemeCodeName: String, encodedKey: ByteArray): PrivateKey {
|
||||
return decodePrivateKey(findSignatureScheme(schemeCodeName), encodedKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a PKCS8 encoded key to its [PrivateKey] object based on the input scheme code name.
|
||||
@ -295,13 +319,18 @@ object Crypto {
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for this key factory to produce a private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeySpecException::class)
|
||||
fun decodePrivateKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PrivateKey {
|
||||
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
|
||||
require(isSupportedSignatureScheme(signatureScheme)) {
|
||||
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
|
||||
}
|
||||
try {
|
||||
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
|
||||
val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
|
||||
return keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey))
|
||||
} catch (ikse: InvalidKeySpecException) {
|
||||
throw InvalidKeySpecException("This private key cannot be decoded, please ensure it is PKCS8 encoded and that it corresponds to the input scheme's code name.", ikse)
|
||||
throw InvalidKeySpecException("This private key cannot be decoded, please ensure it is PKCS8 encoded and that " +
|
||||
"it corresponds to the input scheme's code name.", ikse)
|
||||
}
|
||||
}
|
||||
|
||||
@ -312,11 +341,12 @@ object Crypto {
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for this key factory to produce a private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@JvmStatic
|
||||
fun decodePublicKey(encodedKey: ByteArray): PublicKey {
|
||||
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(encodedKey)
|
||||
val signatureScheme = findSignatureScheme(subjectPublicKeyInfo.algorithm)
|
||||
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
|
||||
val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
|
||||
return keyFactory.generatePublic(X509EncodedKeySpec(encodedKey))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -328,8 +358,11 @@ object Crypto {
|
||||
* @throws InvalidKeySpecException if the given key specification
|
||||
* is inappropriate for this key factory to produce a public key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
|
||||
fun decodePublicKey(schemeCodeName: String, encodedKey: ByteArray): PublicKey = decodePublicKey(findSignatureScheme(schemeCodeName), encodedKey)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeySpecException::class)
|
||||
fun decodePublicKey(schemeCodeName: String, encodedKey: ByteArray): PublicKey {
|
||||
return decodePublicKey(findSignatureScheme(schemeCodeName), encodedKey)
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an X509 encoded key to its [PrivateKey] object based on the input scheme code name.
|
||||
@ -340,19 +373,25 @@ object Crypto {
|
||||
* @throws InvalidKeySpecException if the given key specification
|
||||
* is inappropriate for this key factory to produce a public key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeySpecException::class)
|
||||
fun decodePublicKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PublicKey {
|
||||
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
|
||||
require(isSupportedSignatureScheme(signatureScheme)) {
|
||||
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
|
||||
}
|
||||
try {
|
||||
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
|
||||
val keyFactory = KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
|
||||
return keyFactory.generatePublic(X509EncodedKeySpec(encodedKey))
|
||||
} catch (ikse: InvalidKeySpecException) {
|
||||
throw throw InvalidKeySpecException("This public key cannot be decoded, please ensure it is X509 encoded and that it corresponds to the input scheme's code name.", ikse)
|
||||
throw throw InvalidKeySpecException("This public key cannot be decoded, please ensure it is X509 encoded and " +
|
||||
"that it corresponds to the input scheme's code name.", ikse)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic way to sign [ByteArray] data with a [PrivateKey]. Strategy on on identifying the actual signing scheme is based
|
||||
* on the [PrivateKey] type, but if the schemeCodeName is known, then better use doSign(signatureScheme: String, privateKey: PrivateKey, clearData: ByteArray).
|
||||
* on the [PrivateKey] type, but if the schemeCodeName is known, then better use
|
||||
* doSign(signatureScheme: String, privateKey: PrivateKey, clearData: ByteArray).
|
||||
* @param privateKey the signer's [PrivateKey].
|
||||
* @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root).
|
||||
* @return the digital signature (in [ByteArray]) on the input message.
|
||||
@ -360,7 +399,8 @@ object Crypto {
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doSign(privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(privateKey), privateKey, clearData)
|
||||
|
||||
/**
|
||||
@ -373,8 +413,11 @@ object Crypto {
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(schemeCodeName), privateKey, clearData)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray): ByteArray {
|
||||
return doSign(findSignatureScheme(schemeCodeName), privateKey, clearData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic way to sign [ByteArray] data with a [PrivateKey] and a known [Signature].
|
||||
@ -386,11 +429,14 @@ object Crypto {
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doSign(signatureScheme: SignatureScheme, privateKey: PrivateKey, clearData: ByteArray): ByteArray {
|
||||
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
|
||||
require(isSupportedSignatureScheme(signatureScheme)) {
|
||||
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
|
||||
}
|
||||
require(clearData.isNotEmpty()) { "Signing of an empty array is not permitted!" }
|
||||
val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName])
|
||||
if (clearData.isEmpty()) throw Exception("Signing of an empty array is not permitted!")
|
||||
signature.initSign(privateKey)
|
||||
signature.update(clearData)
|
||||
return signature.sign()
|
||||
@ -398,20 +444,24 @@ object Crypto {
|
||||
|
||||
/**
|
||||
* Generic way to sign [SignableData] objects with a [PrivateKey].
|
||||
* [SignableData] is a wrapper over the transaction's id (Merkle root) in order to attach extra information, such as a timestamp or partial and blind signature indicators.
|
||||
* @param privateKey the signer's [PrivateKey].
|
||||
* [SignableData] is a wrapper over the transaction's id (Merkle root) in order to attach extra information, such as
|
||||
* a timestamp or partial and blind signature indicators.
|
||||
* @param keyPair the signer's [KeyPair].
|
||||
* @param signableData a [SignableData] object that adds extra information to a transaction.
|
||||
* @return a [TransactionSignature] object than contains the output of a successful signing, signer's public key and the signature metadata.
|
||||
* @return a [TransactionSignature] object than contains the output of a successful signing, signer's public key and
|
||||
* the signature metadata.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported for this private key.
|
||||
* @throws InvalidKeyException if the private key is invalid.
|
||||
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doSign(keyPair: KeyPair, signableData: SignableData): TransactionSignature {
|
||||
val sigKey: SignatureScheme = findSignatureScheme(keyPair.private)
|
||||
val sigMetaData: SignatureScheme = findSignatureScheme(keyPair.public)
|
||||
if (sigKey != sigMetaData) throw IllegalArgumentException("Metadata schemeCodeName: ${sigMetaData.schemeCodeName}" +
|
||||
" is not aligned with the key type: ${sigKey.schemeCodeName}.")
|
||||
require(sigKey == sigMetaData) {
|
||||
"Metadata schemeCodeName: ${sigMetaData.schemeCodeName} is not aligned with the key type: ${sigKey.schemeCodeName}."
|
||||
}
|
||||
val signatureBytes = doSign(sigKey.schemeCodeName, keyPair.private, signableData.serialize().bytes)
|
||||
return TransactionSignature(signatureBytes, keyPair.public, signableData.signatureMetadata)
|
||||
}
|
||||
@ -430,8 +480,11 @@ object Crypto {
|
||||
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||
*/
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(schemeCodeName), publicKey, signatureData, clearData)
|
||||
fun doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
|
||||
return doVerify(findSignatureScheme(schemeCodeName), publicKey, signatureData, clearData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type.
|
||||
@ -448,8 +501,11 @@ object Crypto {
|
||||
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
fun doVerify(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(publicKey), publicKey, signatureData, clearData)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doVerify(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
|
||||
return doVerify(findSignatureScheme(publicKey), publicKey, signatureData, clearData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to verify a digital signature.
|
||||
@ -465,9 +521,12 @@ object Crypto {
|
||||
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doVerify(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
|
||||
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
|
||||
require(isSupportedSignatureScheme(signatureScheme)) {
|
||||
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
|
||||
}
|
||||
if (signatureData.isEmpty()) throw IllegalArgumentException("Signature data is empty!")
|
||||
if (clearData.isEmpty()) throw IllegalArgumentException("Clear data is empty, nothing to verify!")
|
||||
val verificationResult = isValid(signatureScheme, publicKey, signatureData, clearData)
|
||||
@ -490,14 +549,16 @@ object Crypto {
|
||||
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
@JvmStatic
|
||||
@Throws(InvalidKeyException::class, SignatureException::class)
|
||||
fun doVerify(txId: SecureHash, transactionSignature: TransactionSignature): Boolean {
|
||||
val signableData = SignableData(txId, transactionSignature.signatureMetadata)
|
||||
return Crypto.doVerify(transactionSignature.by, transactionSignature.bytes, signableData.serialize().bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type.
|
||||
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the
|
||||
* input public key's type.
|
||||
* It returns true if it succeeds and false if not. In comparison to [doVerify] if the key and signature
|
||||
* do not match it returns false rather than throwing an exception. Normally you should use the function which throws,
|
||||
* as it avoids the risk of failing to test the result.
|
||||
@ -507,14 +568,20 @@ object Crypto {
|
||||
* the passed-in signatureData is improperly encoded or of the wrong type,
|
||||
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||
*/
|
||||
@JvmStatic
|
||||
@Throws(SignatureException::class)
|
||||
fun isValid(txId: SecureHash, transactionSignature: TransactionSignature): Boolean {
|
||||
val signableData = SignableData(txId, transactionSignature.signatureMetadata)
|
||||
return isValid(findSignatureScheme(transactionSignature.by), transactionSignature.by, transactionSignature.bytes, signableData.serialize().bytes)
|
||||
return isValid(
|
||||
findSignatureScheme(transactionSignature.by),
|
||||
transactionSignature.by,
|
||||
transactionSignature.bytes,
|
||||
signableData.serialize().bytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type.
|
||||
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the
|
||||
* input public key's type.
|
||||
* It returns true if it succeeds and false if not. In comparison to [doVerify] if the key and signature
|
||||
* do not match it returns false rather than throwing an exception. Normally you should use the function which throws,
|
||||
* as it avoids the risk of failing to test the result.
|
||||
@ -527,8 +594,11 @@ object Crypto {
|
||||
* the passed-in signatureData is improperly encoded or of the wrong type,
|
||||
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||
*/
|
||||
@JvmStatic
|
||||
@Throws(SignatureException::class)
|
||||
fun isValid(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = isValid(findSignatureScheme(publicKey), publicKey, signatureData, clearData)
|
||||
fun isValid(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
|
||||
return isValid(findSignatureScheme(publicKey), publicKey, signatureData, clearData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to verify a digital signature. In comparison to [doVerify] if the key and signature
|
||||
@ -544,9 +614,12 @@ object Crypto {
|
||||
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||
*/
|
||||
@Throws(SignatureException::class, IllegalArgumentException::class)
|
||||
@JvmStatic
|
||||
@Throws(SignatureException::class)
|
||||
fun isValid(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
|
||||
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
|
||||
require(isSupportedSignatureScheme(signatureScheme)) {
|
||||
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
|
||||
}
|
||||
val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName])
|
||||
signature.initVerify(publicKey)
|
||||
signature.update(clearData)
|
||||
@ -560,7 +633,7 @@ object Crypto {
|
||||
* @return a KeyPair for the requested signature scheme code name.
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@JvmStatic
|
||||
fun generateKeyPair(schemeCodeName: String): KeyPair = generateKeyPair(findSignatureScheme(schemeCodeName))
|
||||
|
||||
/**
|
||||
@ -570,10 +643,12 @@ object Crypto {
|
||||
* @return a new [KeyPair] for the requested [SignatureScheme].
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@JvmOverloads
|
||||
@JvmStatic
|
||||
fun generateKeyPair(signatureScheme: SignatureScheme = DEFAULT_SIGNATURE_SCHEME): KeyPair {
|
||||
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
|
||||
require(isSupportedSignatureScheme(signatureScheme)) {
|
||||
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
|
||||
}
|
||||
val keyPairGenerator = KeyPairGenerator.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
|
||||
if (signatureScheme.algSpec != null)
|
||||
keyPairGenerator.initialize(signatureScheme.algSpec, newSecureRandom())
|
||||
@ -638,13 +713,17 @@ object Crypto {
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||
* @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun deriveKeyPair(signatureScheme: SignatureScheme, privateKey: PrivateKey, seed: ByteArray): KeyPair {
|
||||
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
|
||||
when (signatureScheme) {
|
||||
ECDSA_SECP256R1_SHA256, ECDSA_SECP256K1_SHA256 -> return deriveKeyPairECDSA(signatureScheme.algSpec as ECParameterSpec, privateKey, seed)
|
||||
EDDSA_ED25519_SHA512 -> return deriveKeyPairEdDSA(privateKey, seed)
|
||||
require(isSupportedSignatureScheme(signatureScheme)) {
|
||||
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
|
||||
}
|
||||
return when (signatureScheme) {
|
||||
ECDSA_SECP256R1_SHA256, ECDSA_SECP256K1_SHA256 -> deriveKeyPairECDSA(signatureScheme.algSpec as ECParameterSpec, privateKey, seed)
|
||||
EDDSA_ED25519_SHA512 -> deriveKeyPairEdDSA(privateKey, seed)
|
||||
else -> throw UnsupportedOperationException("Although supported for signing, deterministic key derivation is " +
|
||||
"not currently implemented for ${signatureScheme.schemeCodeName}")
|
||||
}
|
||||
throw UnsupportedOperationException("Although supported for signing, deterministic key derivation is not currently implemented for ${signatureScheme.schemeCodeName}")
|
||||
}
|
||||
|
||||
/**
|
||||
@ -656,6 +735,7 @@ object Crypto {
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||
* @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun deriveKeyPair(privateKey: PrivateKey, seed: ByteArray): KeyPair {
|
||||
return deriveKeyPair(findSignatureScheme(privateKey), privateKey, seed)
|
||||
}
|
||||
@ -728,11 +808,13 @@ object Crypto {
|
||||
* @return a new [KeyPair] from an entropy input.
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported for KeyPair generation using an entropy input.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun deriveKeyPairFromEntropy(signatureScheme: SignatureScheme, entropy: BigInteger): KeyPair {
|
||||
when (signatureScheme) {
|
||||
EDDSA_ED25519_SHA512 -> return deriveEdDSAKeyPairFromEntropy(entropy)
|
||||
return when (signatureScheme) {
|
||||
EDDSA_ED25519_SHA512 -> deriveEdDSAKeyPairFromEntropy(entropy)
|
||||
else -> throw IllegalArgumentException("Unsupported signature scheme for fixed entropy-based key pair " +
|
||||
"generation: ${signatureScheme.schemeCodeName}")
|
||||
}
|
||||
throw IllegalArgumentException("Unsupported signature scheme for fixed entropy-based key pair generation: ${signatureScheme.schemeCodeName}")
|
||||
}
|
||||
|
||||
/**
|
||||
@ -740,6 +822,7 @@ object Crypto {
|
||||
* @param entropy a [BigInteger] value.
|
||||
* @return a new [KeyPair] from an entropy input.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun deriveKeyPairFromEntropy(entropy: BigInteger): KeyPair = deriveKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy)
|
||||
|
||||
// custom key pair generator from entropy.
|
||||
@ -766,8 +849,12 @@ object Crypto {
|
||||
}
|
||||
|
||||
private class KeyInfoConverter(val signatureScheme: SignatureScheme) : AsymmetricKeyInfoConverter {
|
||||
override fun generatePublic(keyInfo: SubjectPublicKeyInfo?): PublicKey? = keyInfo?.let { decodePublicKey(signatureScheme, it.encoded) }
|
||||
override fun generatePrivate(keyInfo: PrivateKeyInfo?): PrivateKey? = keyInfo?.let { decodePrivateKey(signatureScheme, it.encoded) }
|
||||
override fun generatePublic(keyInfo: SubjectPublicKeyInfo?): PublicKey? {
|
||||
return keyInfo?.let { decodePublicKey(signatureScheme, it.encoded) }
|
||||
}
|
||||
override fun generatePrivate(keyInfo: PrivateKeyInfo?): PrivateKey? {
|
||||
return keyInfo?.let { decodePrivateKey(signatureScheme, it.encoded) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -784,22 +871,29 @@ object Crypto {
|
||||
* @return true if the point lies on the curve or false if it doesn't.
|
||||
* @throws IllegalArgumentException if the requested signature scheme or the key type is not supported.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@JvmStatic
|
||||
fun publicKeyOnCurve(signatureScheme: SignatureScheme, publicKey: PublicKey): Boolean {
|
||||
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
|
||||
when (publicKey) {
|
||||
is BCECPublicKey -> return (publicKey.parameters == signatureScheme.algSpec && !publicKey.q.isInfinity && publicKey.q.isValid)
|
||||
is EdDSAPublicKey -> return (publicKey.params == signatureScheme.algSpec && !isEdDSAPointAtInfinity(publicKey) && publicKey.a.isOnCurve)
|
||||
require(isSupportedSignatureScheme(signatureScheme)) {
|
||||
"Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}"
|
||||
}
|
||||
return when (publicKey) {
|
||||
is BCECPublicKey -> publicKey.parameters == signatureScheme.algSpec && !publicKey.q.isInfinity && publicKey.q.isValid
|
||||
is EdDSAPublicKey -> publicKey.params == signatureScheme.algSpec && !isEdDSAPointAtInfinity(publicKey) && publicKey.a.isOnCurve
|
||||
else -> throw IllegalArgumentException("Unsupported key type: ${publicKey::class}")
|
||||
}
|
||||
}
|
||||
|
||||
// return true if EdDSA publicKey is point at infinity.
|
||||
// For EdDSA a custom function is required as it is not supported by the I2P implementation.
|
||||
private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey) = publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3)
|
||||
private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey): Boolean {
|
||||
return publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3)
|
||||
}
|
||||
|
||||
/** Check if the requested [SignatureScheme] is supported by the system. */
|
||||
fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean = supportedSignatureSchemes[signatureScheme.schemeCodeName] === signatureScheme
|
||||
@JvmStatic
|
||||
fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean {
|
||||
return signatureScheme.schemeCodeName in signatureSchemeMap
|
||||
}
|
||||
|
||||
// validate a key, by checking its algorithmic params.
|
||||
private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean {
|
||||
@ -812,19 +906,19 @@ object Crypto {
|
||||
|
||||
// check if a public key satisfies algorithm specs (for ECC: key should lie on the curve and not being point-at-infinity).
|
||||
private fun validatePublicKey(signatureScheme: SignatureScheme, key: PublicKey): Boolean {
|
||||
when (key) {
|
||||
is BCECPublicKey, is EdDSAPublicKey -> return publicKeyOnCurve(signatureScheme, key)
|
||||
is BCRSAPublicKey, is BCSphincs256PublicKey -> return true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size).
|
||||
return when (key) {
|
||||
is BCECPublicKey, is EdDSAPublicKey -> publicKeyOnCurve(signatureScheme, key)
|
||||
is BCRSAPublicKey, is BCSphincs256PublicKey -> true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size).
|
||||
else -> throw IllegalArgumentException("Unsupported key type: ${key::class}")
|
||||
}
|
||||
}
|
||||
|
||||
// check if a private key satisfies algorithm specs.
|
||||
private fun validatePrivateKey(signatureScheme: SignatureScheme, key: PrivateKey): Boolean {
|
||||
when (key) {
|
||||
is BCECPrivateKey -> return key.parameters == signatureScheme.algSpec
|
||||
is EdDSAPrivateKey -> return key.params == signatureScheme.algSpec
|
||||
is BCRSAPrivateKey, is BCSphincs256PrivateKey -> return true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size).
|
||||
return when (key) {
|
||||
is BCECPrivateKey -> key.parameters == signatureScheme.algSpec
|
||||
is EdDSAPrivateKey -> key.params == signatureScheme.algSpec
|
||||
is BCRSAPrivateKey, is BCSphincs256PrivateKey -> true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size).
|
||||
else -> throw IllegalArgumentException("Unsupported key type: ${key::class}")
|
||||
}
|
||||
}
|
||||
@ -838,21 +932,20 @@ object Crypto {
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for a supported key factory to produce a private key.
|
||||
*/
|
||||
fun toSupportedPublicKey(key: SubjectPublicKeyInfo): PublicKey {
|
||||
return Crypto.decodePublicKey(key.encoded)
|
||||
}
|
||||
@JvmStatic
|
||||
fun toSupportedPublicKey(key: SubjectPublicKeyInfo): PublicKey = decodePublicKey(key.encoded)
|
||||
|
||||
/**
|
||||
* Convert a public key to a supported implementation. This can be used to convert a SUN's EC key to an BC key.
|
||||
* This method is usually required to retrieve a key (via its corresponding cert) from JKS keystores that by default return SUN implementations.
|
||||
* This method is usually required to retrieve a key (via its corresponding cert) from JKS keystores that by default
|
||||
* return SUN implementations.
|
||||
* @param key a public key.
|
||||
* @return a supported implementation of the input public key.
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for a supported key factory to produce a private key.
|
||||
*/
|
||||
fun toSupportedPublicKey(key: PublicKey): PublicKey {
|
||||
return Crypto.decodePublicKey(key.encoded)
|
||||
}
|
||||
@JvmStatic
|
||||
fun toSupportedPublicKey(key: PublicKey): PublicKey = decodePublicKey(key.encoded)
|
||||
|
||||
/**
|
||||
* Convert a private key to a supported implementation. This can be used to convert a SUN's EC key to an BC key.
|
||||
@ -862,7 +955,6 @@ object Crypto {
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for a supported key factory to produce a private key.
|
||||
*/
|
||||
fun toSupportedPrivateKey(key: PrivateKey): PrivateKey {
|
||||
return Crypto.decodePrivateKey(key.encoded)
|
||||
}
|
||||
@JvmStatic
|
||||
fun toSupportedPrivateKey(key: PrivateKey): PrivateKey = decodePrivateKey(key.encoded)
|
||||
}
|
||||
|
@ -11,9 +11,9 @@ import java.security.SignatureException
|
||||
// should be renamed to match.
|
||||
/** A wrapper around a digital signature. */
|
||||
@CordaSerializable
|
||||
open class DigitalSignature(bits: ByteArray) : OpaqueBytes(bits) {
|
||||
open class DigitalSignature(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||
/** A digital signature that identifies who the public key is owned by. */
|
||||
open class WithKey(val by: PublicKey, bits: ByteArray) : DigitalSignature(bits) {
|
||||
open class WithKey(val by: PublicKey, bytes: ByteArray) : DigitalSignature(bytes) {
|
||||
/**
|
||||
* Utility to simplify the act of verifying a signature.
|
||||
*
|
||||
|
@ -1,6 +1,7 @@
|
||||
@file:JvmName("X500NameUtils")
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.internal.toX509CertHolder
|
||||
import org.bouncycastle.asn1.ASN1Encodable
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x500.X500NameBuilder
|
||||
@ -57,7 +58,7 @@ val X500Name.locationOrNull: String? get() = try {
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
val X509Certificate.subject: X500Name get() = X509CertificateHolder(encoded).subject
|
||||
val X509Certificate.subject: X500Name get() = toX509CertHolder().subject
|
||||
val X509CertificateHolder.cert: X509Certificate get() = JcaX509CertificateConverter().getCertificate(this)
|
||||
|
||||
/**
|
||||
|
@ -42,7 +42,7 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
||||
|
||||
fun getInstance(asn1: ASN1Primitive): PublicKey {
|
||||
val keyInfo = SubjectPublicKeyInfo.getInstance(asn1)
|
||||
require(keyInfo.algorithm.algorithm == CordaObjectIdentifier.compositeKey)
|
||||
require(keyInfo.algorithm.algorithm == CordaObjectIdentifier.COMPOSITE_KEY)
|
||||
val sequence = ASN1Sequence.getInstance(keyInfo.parsePublicKey())
|
||||
val threshold = ASN1Integer.getInstance(sequence.getObjectAt(0)).positiveValue.toInt()
|
||||
val sequenceOfChildren = ASN1Sequence.getInstance(sequence.getObjectAt(1))
|
||||
@ -177,7 +177,7 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
||||
}
|
||||
keyVector.add(ASN1Integer(threshold.toLong()))
|
||||
keyVector.add(DERSequence(childrenVector))
|
||||
return SubjectPublicKeyInfo(AlgorithmIdentifier(CordaObjectIdentifier.compositeKey), DERSequence(keyVector)).encoded
|
||||
return SubjectPublicKeyInfo(AlgorithmIdentifier(CordaObjectIdentifier.COMPOSITE_KEY), DERSequence(keyVector)).encoded
|
||||
}
|
||||
|
||||
override fun getFormat() = ASN1Encoding.DER
|
||||
@ -262,7 +262,9 @@ class CompositeKey private constructor(val threshold: Int, children: List<NodeAn
|
||||
else if (n == 1) {
|
||||
require(threshold == null || threshold == children.first().weight)
|
||||
{ "Trying to build invalid CompositeKey, threshold value different than weight of single child node." }
|
||||
children.first().node // We can assume that this node is a correct CompositeKey.
|
||||
// Returning the only child node which is [PublicKey] itself. We need to avoid single-key [CompositeKey] instances,
|
||||
// as there are scenarios where developers expected the underlying key and its composite versions to be equivalent.
|
||||
children.first().node
|
||||
} else throw IllegalArgumentException("Trying to build CompositeKey without child nodes.")
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import java.security.spec.AlgorithmParameterSpec
|
||||
*/
|
||||
class CompositeSignature : Signature(SIGNATURE_ALGORITHM) {
|
||||
companion object {
|
||||
val SIGNATURE_ALGORITHM = "COMPOSITESIG"
|
||||
const val SIGNATURE_ALGORITHM = "COMPOSITESIG"
|
||||
fun getService(provider: Provider) = Provider.Service(provider, "Signature", SIGNATURE_ALGORITHM, CompositeSignature::class.java.name, emptyList(), emptyMap())
|
||||
}
|
||||
|
||||
|
@ -3,14 +3,13 @@ package net.corda.core.crypto.provider
|
||||
import net.corda.core.crypto.composite.CompositeKey
|
||||
import net.corda.core.crypto.composite.CompositeSignature
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
|
||||
import java.security.AccessController
|
||||
import java.security.PrivilegedAction
|
||||
import java.security.Provider
|
||||
|
||||
class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME security provider wrapper") {
|
||||
companion object {
|
||||
val PROVIDER_NAME = "Corda"
|
||||
const val PROVIDER_NAME = "Corda"
|
||||
}
|
||||
|
||||
init {
|
||||
@ -21,7 +20,7 @@ class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME secur
|
||||
put("KeyFactory.${CompositeKey.KEY_ALGORITHM}", "net.corda.core.crypto.composite.KeyFactory")
|
||||
put("Signature.${CompositeSignature.SIGNATURE_ALGORITHM}", "net.corda.core.crypto.composite.CompositeSignature")
|
||||
|
||||
val compositeKeyOID = CordaObjectIdentifier.compositeKey.id
|
||||
val compositeKeyOID = CordaObjectIdentifier.COMPOSITE_KEY.id
|
||||
put("Alg.Alias.KeyFactory.$compositeKeyOID", CompositeKey.KEY_ALGORITHM)
|
||||
put("Alg.Alias.KeyFactory.OID.$compositeKeyOID", CompositeKey.KEY_ALGORITHM)
|
||||
put("Alg.Alias.Signature.$compositeKeyOID", CompositeSignature.SIGNATURE_ALGORITHM)
|
||||
@ -32,6 +31,6 @@ class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME secur
|
||||
object CordaObjectIdentifier {
|
||||
// UUID-based OID
|
||||
// TODO: Register for an OID space and issue our own shorter OID
|
||||
val compositeKey = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791002")
|
||||
val compositeSignature = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003")
|
||||
@JvmField val COMPOSITE_KEY = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791002")
|
||||
@JvmField val COMPOSITE_SIGNATURE = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003")
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
@ -56,12 +57,14 @@ import java.security.PublicKey
|
||||
* val stx = subFlow(CollectSignaturesFlow(ptx))
|
||||
*
|
||||
* @param partiallySignedTx Transaction to collect the remaining signatures for
|
||||
* @param myOptionalKeys set of keys in the transaction which are owned by this node. This includes keys used on commands, not
|
||||
* just in the states. If null, the default well known identity of the node is used.
|
||||
*/
|
||||
// TODO: AbstractStateReplacementFlow needs updating to use this flow.
|
||||
// TODO: Update this flow to handle randomly generated keys when that work is complete.
|
||||
class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
|
||||
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
class CollectSignaturesFlow @JvmOverloads constructor (val partiallySignedTx: SignedTransaction,
|
||||
val myOptionalKeys: Iterable<PublicKey>?,
|
||||
override val progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
@JvmOverloads constructor(partiallySignedTx: SignedTransaction, progressTracker: ProgressTracker = CollectSignaturesFlow.tracker()) : this(partiallySignedTx, null, progressTracker)
|
||||
companion object {
|
||||
object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.")
|
||||
object VERIFYING : ProgressTracker.Step("Verifying collected signatures.")
|
||||
@ -72,16 +75,14 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
|
||||
}
|
||||
|
||||
@Suspendable override fun call(): SignedTransaction {
|
||||
// TODO: Revisit when key management is properly fleshed out.
|
||||
// This will break if a party uses anything other than their legalIdentityKey.
|
||||
// Check the signatures which have already been provided and that the transaction is valid.
|
||||
// Usually just the Initiator and possibly an oracle would have signed at this point.
|
||||
val myKey = serviceHub.myInfo.legalIdentity.owningKey
|
||||
val myKeys: Iterable<PublicKey> = myOptionalKeys ?: listOf(serviceHub.myInfo.legalIdentity.owningKey)
|
||||
val signed = partiallySignedTx.sigs.map { it.by }
|
||||
val notSigned = partiallySignedTx.tx.requiredSigningKeys - signed
|
||||
|
||||
// One of the signatures collected so far MUST be from the initiator of this flow.
|
||||
require(partiallySignedTx.sigs.any { it.by == myKey }) {
|
||||
require(partiallySignedTx.sigs.any { it.by in myKeys }) {
|
||||
"The Initiator of CollectSignaturesFlow must have signed the transaction."
|
||||
}
|
||||
|
||||
@ -100,7 +101,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
|
||||
if (unsigned.isEmpty()) return partiallySignedTx
|
||||
|
||||
// Collect signatures from all counter-parties and append them to the partially signed transaction.
|
||||
val counterpartySignatures = keysToParties(unsigned).map { collectSignature(it) }
|
||||
val counterpartySignatures = keysToParties(unsigned).map { collectSignature(it.first, it.second) }
|
||||
val stx = partiallySignedTx + counterpartySignatures
|
||||
|
||||
// Verify all but the notary's signature if the transaction requires a notary, otherwise verify all signatures.
|
||||
@ -112,23 +113,32 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
|
||||
|
||||
/**
|
||||
* Lookup the [Party] object for each [PublicKey] using the [ServiceHub.networkMapCache].
|
||||
*
|
||||
* @return a pair of the well known identity to contact for a signature, and the public key that party should sign
|
||||
* with (this may belong to a confidential identity).
|
||||
*/
|
||||
@Suspendable private fun keysToParties(keys: Collection<PublicKey>): List<Party> = keys.map {
|
||||
// TODO: Revisit when IdentityService supports resolution of a (possibly random) public key to a legal identity key.
|
||||
val partyNode = serviceHub.networkMapCache.getNodeByLegalIdentityKey(it)
|
||||
@Suspendable private fun keysToParties(keys: Collection<PublicKey>): List<Pair<Party, PublicKey>> = keys.map {
|
||||
val party = serviceHub.identityService.partyFromAnonymous(AnonymousParty(it))
|
||||
?: throw IllegalStateException("Party ${it.toBase58String()} not found on the network.")
|
||||
partyNode.legalIdentity
|
||||
Pair(party, it)
|
||||
}
|
||||
|
||||
// DOCSTART 1
|
||||
/**
|
||||
* Get and check the required signature.
|
||||
*
|
||||
* @param counterparty the party to request a signature from.
|
||||
* @param signingKey the key the party should use to sign the transaction.
|
||||
*/
|
||||
@Suspendable private fun collectSignature(counterparty: Party): TransactionSignature {
|
||||
// SendTransactionFlow allows otherParty to access our data to resolve the transaction.
|
||||
@Suspendable private fun collectSignature(counterparty: Party, signingKey: PublicKey): TransactionSignature {
|
||||
// SendTransactionFlow allows counterparty to access our data to resolve the transaction.
|
||||
subFlow(SendTransactionFlow(counterparty, partiallySignedTx))
|
||||
// Send the key we expect the counterparty to sign with - this is important where they may have several
|
||||
// keys to sign with, as it makes it faster for them to identify the key to sign with, and more straight forward
|
||||
// for us to check we have the expected signature returned.
|
||||
send(counterparty, signingKey)
|
||||
return receive<TransactionSignature>(counterparty).unwrap {
|
||||
require(counterparty.owningKey.isFulfilledBy(it.by)) { "Not signed by the required Party." }
|
||||
require(signingKey.isFulfilledBy(it.by)) { "Not signed by the required signing key." }
|
||||
it
|
||||
}
|
||||
}
|
||||
@ -189,9 +199,16 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
progressTracker.currentStep = RECEIVING
|
||||
// Receive transaction and resolve dependencies, check sufficient signatures is disabled as we don't have all signatures.
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherParty, checkSufficientSignatures = false))
|
||||
// Receive the signing key that the party requesting the signature expects us to sign with. Having this provided
|
||||
// means we only have to check we own that one key, rather than matching all keys in the transaction against all
|
||||
// keys we own.
|
||||
val signingKey = receive<PublicKey>(otherParty).unwrap {
|
||||
// TODO: We should have a faster way of verifying we own a single key
|
||||
serviceHub.keyManagementService.filterMyKeys(listOf(it)).single()
|
||||
}
|
||||
progressTracker.currentStep = VERIFYING
|
||||
// Check that the Responder actually needs to sign.
|
||||
checkMySignatureRequired(stx)
|
||||
checkMySignatureRequired(stx, signingKey)
|
||||
// Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's.
|
||||
checkSignatures(stx)
|
||||
stx.tx.toLedgerTransaction(serviceHub).verify()
|
||||
@ -206,7 +223,7 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
}
|
||||
// Sign and send back our signature to the Initiator.
|
||||
progressTracker.currentStep = SIGNING
|
||||
val mySignature = serviceHub.createSignature(stx)
|
||||
val mySignature = serviceHub.createSignature(stx, signingKey)
|
||||
send(otherParty, mySignature)
|
||||
|
||||
// Return the fully signed transaction once it has been committed.
|
||||
@ -214,8 +231,10 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
}
|
||||
|
||||
@Suspendable private fun checkSignatures(stx: SignedTransaction) {
|
||||
require(stx.sigs.any { it.by == otherParty.owningKey }) {
|
||||
"The Initiator of CollectSignaturesFlow must have signed the transaction."
|
||||
val signingIdentities = stx.sigs.map(TransactionSignature::by).mapNotNull(serviceHub.identityService::partyFromKey)
|
||||
val signingWellKnownIdentities = signingIdentities.mapNotNull(serviceHub.identityService::partyFromAnonymous)
|
||||
require(otherParty in signingWellKnownIdentities) {
|
||||
"The Initiator of CollectSignaturesFlow must have signed the transaction. Found ${signingWellKnownIdentities}, expected ${otherParty}"
|
||||
}
|
||||
val signed = stx.sigs.map { it.by }
|
||||
val allSigners = stx.tx.requiredSigningKeys
|
||||
@ -245,10 +264,8 @@ abstract class SignTransactionFlow(val otherParty: Party,
|
||||
*/
|
||||
@Suspendable abstract protected fun checkTransaction(stx: SignedTransaction)
|
||||
|
||||
@Suspendable private fun checkMySignatureRequired(stx: SignedTransaction) {
|
||||
// TODO: Revisit when key management is properly fleshed out.
|
||||
val myKey = serviceHub.myInfo.legalIdentity.owningKey
|
||||
require(myKey in stx.tx.requiredSigningKeys) {
|
||||
@Suspendable private fun checkMySignatureRequired(stx: SignedTransaction, signingKey: PublicKey) {
|
||||
require(signingKey in stx.tx.requiredSigningKeys) {
|
||||
"Party is not a participant for any of the input states of transaction ${stx.id}"
|
||||
}
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ abstract class FlowLogic<out T> {
|
||||
* network's event horizon time.
|
||||
*/
|
||||
@Suspendable
|
||||
open fun send(otherParty: Party, payload: Any): Unit = stateMachine.send(otherParty, payload, flowUsedForSessions)
|
||||
open fun send(otherParty: Party, payload: Any) = stateMachine.send(otherParty, payload, flowUsedForSessions)
|
||||
|
||||
/**
|
||||
* Invokes the given subflow. This function returns once the subflow completes successfully with the result
|
||||
@ -239,7 +239,7 @@ abstract class FlowLogic<out T> {
|
||||
* Returns a shallow copy of the Quasar stack frames at the time of call to [flowStackSnapshot]. Use this to inspect
|
||||
* what objects would be serialised at the time of call to a suspending action (e.g. send/receive).
|
||||
* Note: This logic is only available during tests and is not meant to be used during the production deployment.
|
||||
* Therefore the default implementationdoes nothing.
|
||||
* Therefore the default implementation does nothing.
|
||||
*/
|
||||
@Suspendable
|
||||
fun flowStackSnapshot(): FlowStackSnapshot? = stateMachine.flowStackSnapshot(this::class.java)
|
||||
@ -256,7 +256,7 @@ abstract class FlowLogic<out T> {
|
||||
* Therefore the default implementation does nothing.
|
||||
*/
|
||||
@Suspendable
|
||||
fun persistFlowStackSnapshot(): Unit = stateMachine.persistFlowStackSnapshot(this::class.java)
|
||||
fun persistFlowStackSnapshot() = stateMachine.persistFlowStackSnapshot(this::class.java)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
@ -1,69 +1,21 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
|
||||
interface FlowStackSnapshotFactory {
|
||||
private object Holder {
|
||||
val INSTANCE: FlowStackSnapshotFactory
|
||||
|
||||
init {
|
||||
val serviceFactory = ServiceLoader.load(FlowStackSnapshotFactory::class.java).singleOrNull()
|
||||
INSTANCE = serviceFactory ?: FlowStackSnapshotDefaultFactory()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val instance: FlowStackSnapshotFactory by lazy { Holder.INSTANCE }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns flow stack data snapshot extracted from Quasar stack.
|
||||
* It is designed to be used in the debug mode of the flow execution.
|
||||
* Note. This logic is only available during tests and is not meant to be used during the production deployment.
|
||||
* Therefore the default implementation does nothing.
|
||||
*/
|
||||
fun getFlowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot?
|
||||
|
||||
/** Stores flow stack snapshot as a json file. The stored shapshot is only partial and consists
|
||||
* only data (i.e. stack traces and local variables values) relevant to the flow. It does not
|
||||
* persist corda internal data (e.g. FlowStateMachine). Instead it uses [StackFrameDataToken] to indicate
|
||||
* the class of the element on the stack.
|
||||
* The flow stack snapshot is stored in a file located in
|
||||
* {baseDir}/flowStackSnapshots/YYYY-MM-DD/{flowId}/
|
||||
* where baseDir is the node running directory and flowId is the flow unique identifier generated by the platform.
|
||||
* Note. This logic is only available during tests and is not meant to be used during the production deployment.
|
||||
* Therefore the default implementation does nothing.
|
||||
*/
|
||||
fun persistAsJsonFile(flowClass: Class<*>, baseDir: Path, flowId: String): Unit
|
||||
}
|
||||
|
||||
private class FlowStackSnapshotDefaultFactory : FlowStackSnapshotFactory {
|
||||
val log = loggerFor<FlowStackSnapshotDefaultFactory>()
|
||||
|
||||
override fun getFlowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot? {
|
||||
log.warn("Flow stack snapshot are not supposed to be used in a production deployment")
|
||||
return null
|
||||
}
|
||||
|
||||
override fun persistAsJsonFile(flowClass: Class<*>, baseDir: Path, flowId: String) {
|
||||
log.warn("Flow stack snapshot are not supposed to be used in a production deployment")
|
||||
}
|
||||
}
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Main data object representing snapshot of the flow stack, extracted from the Quasar stack.
|
||||
*/
|
||||
data class FlowStackSnapshot constructor(
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
val flowClass: String? = null,
|
||||
val stackFrames: List<Frame> = listOf()
|
||||
data class FlowStackSnapshot(
|
||||
val time: Instant,
|
||||
val flowClass: String,
|
||||
val stackFrames: List<Frame>
|
||||
) {
|
||||
data class Frame(
|
||||
val stackTraceElement: StackTraceElement? = null, // This should be the call that *pushed* the frame of [objects]
|
||||
val stackObjects: List<Any?> = listOf()
|
||||
)
|
||||
val stackTraceElement: StackTraceElement, // This should be the call that *pushed* the frame of [objects]
|
||||
val stackObjects: List<Any?>
|
||||
) {
|
||||
override fun toString(): String = stackTraceElement.toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,96 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.unwrap
|
||||
|
||||
object IdentitySyncFlow {
|
||||
/**
|
||||
* Flow for ensuring that one or more counterparties to a transaction have the full certificate paths of confidential
|
||||
* identities used in the transaction. This is intended for use as a subflow of another flow, typically between
|
||||
* transaction assembly and signing. An example of where this is useful is where a recipient of a [Cash] state wants
|
||||
* to know that it is being paid by the correct party, and the owner of the state is a confidential identity of that
|
||||
* party. This flow would send a copy of the confidential identity path to the recipient, enabling them to verify that
|
||||
* identity.
|
||||
*
|
||||
* @return a mapping of well known identities to the confidential identities used in the transaction.
|
||||
*/
|
||||
// TODO: Can this be triggered automatically from [SendTransactionFlow]
|
||||
class Send(val otherSides: Set<Party>,
|
||||
val tx: WireTransaction,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<Unit>() {
|
||||
constructor(otherSide: Party, tx: WireTransaction) : this(setOf(otherSide), tx, tracker())
|
||||
|
||||
companion object {
|
||||
object SYNCING_IDENTITIES : ProgressTracker.Step("Syncing identities")
|
||||
|
||||
fun tracker() = ProgressTracker(SYNCING_IDENTITIES)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
progressTracker.currentStep = SYNCING_IDENTITIES
|
||||
val states: List<ContractState> = (tx.inputs.map { serviceHub.loadState(it) }.requireNoNulls().map { it.data } + tx.outputs.map { it.data })
|
||||
val identities: Set<AbstractParty> = states.flatMap { it.participants }.toSet()
|
||||
// Filter participants down to the set of those not in the network map (are not well known)
|
||||
val confidentialIdentities = identities
|
||||
.filter { serviceHub.networkMapCache.getNodeByLegalIdentityKey(it.owningKey) == null }
|
||||
.toList()
|
||||
val identityCertificates: Map<AbstractParty, PartyAndCertificate?> = identities
|
||||
.map { Pair(it, serviceHub.identityService.certificateFromKey(it.owningKey)) }.toMap()
|
||||
|
||||
otherSides.forEach { otherSide ->
|
||||
val requestedIdentities: List<AbstractParty> = sendAndReceive<List<AbstractParty>>(otherSide, confidentialIdentities).unwrap { req ->
|
||||
require(req.all { it in identityCertificates.keys }) { "${otherSide} requested a confidential identity not part of transaction: ${tx.id}" }
|
||||
req
|
||||
}
|
||||
val sendIdentities: List<PartyAndCertificate?> = requestedIdentities.map {
|
||||
val identityCertificate = identityCertificates[it]
|
||||
if (identityCertificate != null)
|
||||
identityCertificate
|
||||
else
|
||||
throw IllegalStateException("Counterparty requested a confidential identity for which we do not have the certificate path: ${tx.id}")
|
||||
}
|
||||
send(otherSide, sendIdentities)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an offer to provide proof of identity (in the form of certificate paths) for confidential identities which
|
||||
* we do not yet know about.
|
||||
*/
|
||||
class Receive(val otherSide: Party) : FlowLogic<Unit>() {
|
||||
companion object {
|
||||
object RECEIVING_IDENTITIES : ProgressTracker.Step("Receiving confidential identities")
|
||||
object RECEIVING_CERTIFICATES : ProgressTracker.Step("Receiving certificates for unknown identities")
|
||||
}
|
||||
|
||||
override val progressTracker: ProgressTracker = ProgressTracker(RECEIVING_IDENTITIES, RECEIVING_CERTIFICATES)
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Unit {
|
||||
progressTracker.currentStep = RECEIVING_IDENTITIES
|
||||
val allIdentities = receive<List<AbstractParty>>(otherSide).unwrap { it }
|
||||
val unknownIdentities = allIdentities.filter { serviceHub.identityService.partyFromAnonymous(it) == null }
|
||||
progressTracker.currentStep = RECEIVING_CERTIFICATES
|
||||
val missingIdentities = sendAndReceive<List<PartyAndCertificate>>(otherSide, unknownIdentities)
|
||||
|
||||
// Batch verify the identities we've received, so we know they're all correct before we start storing them in
|
||||
// the identity service
|
||||
missingIdentities.unwrap { identities ->
|
||||
identities.forEach { it.verify(serviceHub.identityService.trustAnchor) }
|
||||
identities
|
||||
}.forEach { identity ->
|
||||
// Store the received confidential identities in the identity service so we have a record of which well known identity they map to.
|
||||
serviceHub.identityService.verifyAndRegisterIdentity(identity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -13,8 +13,7 @@ import java.security.PublicKey
|
||||
@CordaSerializable
|
||||
abstract class AbstractParty(val owningKey: PublicKey) {
|
||||
/** Anonymised parties do not include any detail apart from owning key, so equality is dependent solely on the key */
|
||||
override fun equals(other: Any?): Boolean = other is AbstractParty && this.owningKey == other.owningKey
|
||||
|
||||
override fun equals(other: Any?): Boolean = other === this || other is AbstractParty && other.owningKey == owningKey
|
||||
override fun hashCode(): Int = owningKey.hashCode()
|
||||
abstract fun nameOrNull(): X500Name?
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.corda.core.identity
|
||||
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
@ -12,11 +11,7 @@ import java.security.PublicKey
|
||||
* information such as name. It is intended to represent a party on the distributed ledger.
|
||||
*/
|
||||
class AnonymousParty(owningKey: PublicKey) : AbstractParty(owningKey) {
|
||||
// Use the key as the bulk of the toString(), but include a human readable identifier as well, so that [Party]
|
||||
// can put in the key and actual name
|
||||
override fun toString() = "${owningKey.toStringShort()} <Anonymous>"
|
||||
|
||||
override fun nameOrNull(): X500Name? = null
|
||||
|
||||
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
|
||||
override fun toString() = "Anonymous(${owningKey.toStringShort()})"
|
||||
}
|
@ -1,9 +1,11 @@
|
||||
package net.corda.core.identity
|
||||
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.crypto.CertificateAndKeyPair
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.composite.CompositeKey
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
@ -26,10 +28,9 @@ import java.security.PublicKey
|
||||
* @see CompositeKey
|
||||
*/
|
||||
class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) {
|
||||
constructor(certAndKey: CertificateAndKeyPair) : this(certAndKey.certificate.subject, certAndKey.keyPair.public)
|
||||
override fun toString() = name.toString()
|
||||
override fun nameOrNull(): X500Name? = name
|
||||
|
||||
constructor(certificate: X509CertificateHolder) : this(certificate.subject, Crypto.toSupportedPublicKey(certificate.subjectPublicKeyInfo))
|
||||
override fun nameOrNull(): X500Name = name
|
||||
fun anonymise(): AnonymousParty = AnonymousParty(owningKey)
|
||||
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
|
||||
override fun toString() = name.toString()
|
||||
}
|
||||
|
@ -1,49 +1,42 @@
|
||||
package net.corda.core.identity
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.internal.toX509CertHolder
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.*
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* A full party plus the X.509 certificate and path linking the party back to a trust root. Equality of
|
||||
* [PartyAndCertificate] instances is based on the party only, as certificate and path are data associated with the party,
|
||||
* not part of the identifier themselves. While party and certificate can both be derived from the certificate path,
|
||||
* this class exists in order to ensure the implementation classes of certificates and party public keys are kept stable.
|
||||
* not part of the identifier themselves.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class PartyAndCertificate(val party: Party,
|
||||
val certificate: X509CertificateHolder,
|
||||
val certPath: CertPath) {
|
||||
constructor(name: X500Name, owningKey: PublicKey, certificate: X509CertificateHolder, certPath: CertPath) : this(Party(name, owningKey), certificate, certPath)
|
||||
val name: X500Name
|
||||
get() = party.name
|
||||
val owningKey: PublicKey
|
||||
get() = party.owningKey
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return if (other is PartyAndCertificate)
|
||||
party == other.party
|
||||
else
|
||||
false
|
||||
//TODO Is VerifiableIdentity a better name?
|
||||
class PartyAndCertificate(val certPath: CertPath) {
|
||||
@Transient val certificate: X509CertificateHolder
|
||||
init {
|
||||
require(certPath.type == "X.509") { "Only X.509 certificates supported" }
|
||||
val certs = certPath.certificates
|
||||
require(certs.size >= 2) { "Certificate path must at least include subject and issuing certificates" }
|
||||
certificate = certs[0].toX509CertHolder()
|
||||
}
|
||||
|
||||
@Transient val party: Party = Party(certificate)
|
||||
|
||||
val owningKey: PublicKey get() = party.owningKey
|
||||
val name: X500Name get() = party.name
|
||||
|
||||
operator fun component1(): Party = party
|
||||
operator fun component2(): X509CertificateHolder = certificate
|
||||
|
||||
override fun equals(other: Any?): Boolean = other === this || other is PartyAndCertificate && other.party == party
|
||||
override fun hashCode(): Int = party.hashCode()
|
||||
override fun toString(): String = party.toString()
|
||||
|
||||
/**
|
||||
* Verify that the given certificate path is valid and leads to the owning key of the party.
|
||||
*/
|
||||
/** Verify the certificate path is valid. */
|
||||
fun verify(trustAnchor: TrustAnchor): PKIXCertPathValidatorResult {
|
||||
require(certPath.certificates.first() is X509Certificate) { "Subject certificate must be an X.509 certificate" }
|
||||
require(Arrays.equals(party.owningKey.encoded, certificate.subjectPublicKeyInfo.encoded)) { "Certificate public key must match party owning key" }
|
||||
require(Arrays.equals(certPath.certificates.first().encoded, certificate.encoded)) { "Certificate path must link to certificate" }
|
||||
|
||||
val validatorParameters = PKIXParameters(setOf(trustAnchor))
|
||||
val parameters = PKIXParameters(setOf(trustAnchor)).apply { isRevocationEnabled = false }
|
||||
val validator = CertPathValidator.getInstance("PKIX")
|
||||
validatorParameters.isRevocationEnabled = false
|
||||
return validator.validate(certPath, validatorParameters) as PKIXCertPathValidatorResult
|
||||
return validator.validate(certPath, parameters) as PKIXCertPathValidatorResult
|
||||
}
|
||||
}
|
||||
|
@ -3,11 +3,7 @@ package net.corda.core.internal
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowContext
|
||||
import net.corda.core.flows.FlowInitiator
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowStackSnapshot
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
@ -39,24 +35,11 @@ interface FlowStateMachine<R> {
|
||||
|
||||
fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String, String>): Unit
|
||||
|
||||
/**
|
||||
* Returns a shallow copy of the Quasar stack frames at the time of call to [flowStackSnapshot]. Use this to inspect
|
||||
* what objects would be serialised at the time of call to a suspending action (e.g. send/receive).
|
||||
*/
|
||||
@Suspendable
|
||||
fun flowStackSnapshot(flowClass: Class<*>): FlowStackSnapshot?
|
||||
fun flowStackSnapshot(flowClass: Class<out FlowLogic<*>>): FlowStackSnapshot?
|
||||
|
||||
/**
|
||||
* Persists a shallow copy of the Quasar stack frames at the time of call to [persistFlowStackSnapshot].
|
||||
* Use this to track the monitor evolution of the quasar stack values during the flow execution.
|
||||
* The flow stack snapshot is stored in a file located in {baseDir}/flowStackSnapshots/YYYY-MM-DD/{flowId}/
|
||||
* where baseDir is the node running directory and flowId is the flow unique identifier generated by the platform.
|
||||
*
|
||||
* Note: With respect to the [flowStackSnapshot], the snapshot being persisted by this method is partial,
|
||||
* meaning that only flow relevant traces and local variables are persisted.
|
||||
*/
|
||||
@Suspendable
|
||||
fun persistFlowStackSnapshot(flowClass: Class<*>): Unit
|
||||
fun persistFlowStackSnapshot(flowClass: Class<out FlowLogic<*>>): Unit
|
||||
|
||||
val serviceHub: ServiceHub
|
||||
val logger: Logger
|
||||
|
@ -2,6 +2,7 @@ package net.corda.core.internal
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import org.slf4j.Logger
|
||||
import rx.Observable
|
||||
import rx.Observer
|
||||
@ -165,6 +166,9 @@ fun <T> logElapsedTime(label: String, logger: Logger? = null, body: () -> T): T
|
||||
}
|
||||
}
|
||||
|
||||
fun java.security.cert.Certificate.toX509CertHolder() = X509CertificateHolder(encoded)
|
||||
fun javax.security.cert.Certificate.toX509CertHolder() = X509CertificateHolder(encoded)
|
||||
|
||||
/** Convert a [ByteArrayOutputStream] to [InputStreamAndHash]. */
|
||||
fun ByteArrayOutputStream.toInputStreamAndHash(): InputStreamAndHash {
|
||||
val bytes = toByteArray()
|
||||
|
@ -92,7 +92,7 @@ class ResolveTransactionsFlow(private val txHashes: Set<SecureHash>,
|
||||
// half way through, it's no big deal, although it might result in us attempting to re-download data
|
||||
// redundantly next time we attempt verification.
|
||||
it.verify(serviceHub)
|
||||
serviceHub.recordTransactions(it)
|
||||
serviceHub.recordTransactions(false, it)
|
||||
}
|
||||
|
||||
return signedTransaction?.let {
|
||||
|
@ -1,6 +1,6 @@
|
||||
package net.corda.core.internal.concurrent
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.concurrent.match
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
|
@ -421,6 +421,29 @@ inline fun <T : Any, A, B, C, D, reified R : FlowLogic<T>> CordaRPCOps.startTrac
|
||||
arg3: D
|
||||
): FlowProgressHandle<T> = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3)
|
||||
|
||||
@Suppress("unused")
|
||||
inline fun <T : Any, A, B, C, D, E, reified R : FlowLogic<T>> CordaRPCOps.startTrackedFlow(
|
||||
@Suppress("unused_parameter")
|
||||
flowConstructor: (A, B, C, D, E) -> R,
|
||||
arg0: A,
|
||||
arg1: B,
|
||||
arg2: C,
|
||||
arg3: D,
|
||||
arg4: E
|
||||
): FlowProgressHandle<T> = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4)
|
||||
|
||||
@Suppress("unused")
|
||||
inline fun <T : Any, A, B, C, D, E, F, reified R : FlowLogic<T>> CordaRPCOps.startTrackedFlow(
|
||||
@Suppress("unused_parameter")
|
||||
flowConstructor: (A, B, C, D, E, F) -> R,
|
||||
arg0: A,
|
||||
arg1: B,
|
||||
arg2: C,
|
||||
arg3: D,
|
||||
arg4: E,
|
||||
arg5: F
|
||||
): FlowProgressHandle<T> = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4, arg5)
|
||||
|
||||
/**
|
||||
* The Data feed contains a snapshot of the requested data and an [Observable] of future updates.
|
||||
*/
|
||||
|
@ -1,7 +1,10 @@
|
||||
package net.corda.core.messaging
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
|
||||
|
||||
/** The interface for a group of message recipients (which may contain only one recipient) */
|
||||
@CordaSerializable
|
||||
interface MessageRecipients
|
||||
|
||||
/** A base class for the case of point-to-point messages */
|
||||
|
@ -24,16 +24,17 @@ data class NodeInfo(val addresses: List<NetworkHostAndPort>,
|
||||
val legalIdentityAndCert: PartyAndCertificate, //TODO This field will be removed in future PR which gets rid of services.
|
||||
val legalIdentitiesAndCerts: NonEmptySet<PartyAndCertificate>,
|
||||
val platformVersion: Int,
|
||||
var advertisedServices: List<ServiceEntry> = emptyList(),
|
||||
val advertisedServices: List<ServiceEntry> = emptyList(),
|
||||
val worldMapLocation: WorldMapLocation? = null) {
|
||||
init {
|
||||
require(advertisedServices.none { it.identity == legalIdentityAndCert }) { "Service identities must be different from node legal identity" }
|
||||
require(advertisedServices.none { it.identity == legalIdentityAndCert }) {
|
||||
"Service identities must be different from node legal identity"
|
||||
}
|
||||
}
|
||||
val legalIdentity: Party
|
||||
get() = legalIdentityAndCert.party
|
||||
val notaryIdentity: Party
|
||||
get() = advertisedServices.single { it.info.type.isNotary() }.identity.party
|
||||
|
||||
val legalIdentity: Party get() = legalIdentityAndCert.party
|
||||
val notaryIdentity: Party get() = advertisedServices.single { it.info.type.isNotary() }.identity.party
|
||||
fun serviceIdentities(type: ServiceType): List<Party> {
|
||||
return advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity.party }
|
||||
return advertisedServices.mapNotNull { if (it.info.type.isSubTypeOf(type)) it.identity.party else null }
|
||||
}
|
||||
}
|
||||
|
@ -68,18 +68,35 @@ interface ServiceHub : ServicesForResolution {
|
||||
|
||||
/**
|
||||
* Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for
|
||||
* further processing. This is expected to be run within a database transaction.
|
||||
* further processing if [notifyVault] is true. This is expected to be run within a database transaction.
|
||||
*
|
||||
* @param txs The transactions to record.
|
||||
* @param notifyVault indicate if the vault should be notified for the update.
|
||||
*/
|
||||
fun recordTransactions(txs: Iterable<SignedTransaction>)
|
||||
fun recordTransactions(notifyVault: Boolean, txs: Iterable<SignedTransaction>)
|
||||
|
||||
/**
|
||||
* Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for
|
||||
* further processing if [notifyVault] is true. This is expected to be run within a database transaction.
|
||||
*/
|
||||
fun recordTransactions(notifyVault: Boolean, first: SignedTransaction, vararg remaining: SignedTransaction) {
|
||||
recordTransactions(notifyVault, listOf(first, *remaining))
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for
|
||||
* further processing. This is expected to be run within a database transaction.
|
||||
*/
|
||||
fun recordTransactions(first: SignedTransaction, vararg remaining: SignedTransaction) {
|
||||
recordTransactions(listOf(first, *remaining))
|
||||
recordTransactions(true, first, *remaining)
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for
|
||||
* further processing. This is expected to be run within a database transaction.
|
||||
*/
|
||||
fun recordTransactions(txs: Iterable<SignedTransaction>) {
|
||||
recordTransactions(true, txs)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -92,8 +109,7 @@ interface ServiceHub : ServicesForResolution {
|
||||
val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
|
||||
return if (stx.isNotaryChangeTransaction()) {
|
||||
stx.resolveNotaryChangeTransaction(this).outputs[stateRef.index]
|
||||
}
|
||||
else stx.tx.outputs[stateRef.index]
|
||||
} else stx.tx.outputs[stateRef.index]
|
||||
}
|
||||
|
||||
/**
|
||||
@ -106,8 +122,7 @@ interface ServiceHub : ServicesForResolution {
|
||||
val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
|
||||
return if (stx.isNotaryChangeTransaction()) {
|
||||
stx.resolveNotaryChangeTransaction(this).outRef<T>(stateRef.index)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
stx.tx.outRef<T>(stateRef.index)
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,10 @@
|
||||
package net.corda.core.node.services
|
||||
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.identity.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.cert.X509CertificateHolder
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
@ -114,6 +117,6 @@ interface IdentityService {
|
||||
* @param exactMatch If true, a case sensitive match is done against each component of each X.500 name.
|
||||
*/
|
||||
fun partiesFromName(query: String, exactMatch: Boolean): Set<Party>
|
||||
|
||||
class UnknownAnonymousPartyException(msg: String) : Exception(msg)
|
||||
}
|
||||
|
||||
class UnknownAnonymousPartyException(msg: String) : Exception(msg)
|
||||
|
@ -5,6 +5,7 @@ import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.node.services.vault.QueryCriteria
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.toFuture
|
||||
@ -135,8 +136,7 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
|
||||
val recordedTime: Instant,
|
||||
val consumedTime: Instant?,
|
||||
val status: Vault.StateStatus,
|
||||
val notaryName: String,
|
||||
val notaryKey: String,
|
||||
val notary: AbstractParty?,
|
||||
val lockId: String?,
|
||||
val lockUpdateTime: Instant?)
|
||||
}
|
||||
|
@ -4,12 +4,12 @@ package net.corda.core.node.services.vault
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.schemas.PersistentState
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import javax.persistence.criteria.Predicate
|
||||
@ -40,6 +40,7 @@ sealed class QueryCriteria {
|
||||
|
||||
abstract class CommonQueryCriteria : QueryCriteria() {
|
||||
abstract val status: Vault.StateStatus
|
||||
abstract val contractStateTypes: Set<Class<out ContractState>>?
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
@ -49,13 +50,14 @@ sealed class QueryCriteria {
|
||||
* VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates]
|
||||
*/
|
||||
data class VaultQueryCriteria @JvmOverloads constructor (override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
val contractStateTypes: Set<Class<out ContractState>>? = null,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null,
|
||||
val stateRefs: List<StateRef>? = null,
|
||||
val notaryName: List<X500Name>? = null,
|
||||
val notary: List<AbstractParty>? = null,
|
||||
val softLockingCondition: SoftLockingCondition? = null,
|
||||
val timeCondition: TimeCondition? = null) : CommonQueryCriteria() {
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this))
|
||||
super.visit(parser)
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,9 +67,16 @@ sealed class QueryCriteria {
|
||||
data class LinearStateQueryCriteria @JvmOverloads constructor(val participants: List<AbstractParty>? = null,
|
||||
val uuid: List<UUID>? = null,
|
||||
val externalId: List<String>? = null,
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() {
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null) : CommonQueryCriteria() {
|
||||
constructor(participants: List<AbstractParty>? = null,
|
||||
linearId: List<UniqueIdentifier>? = null,
|
||||
status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
contractStateTypes: Set<Class<out ContractState>>? = null) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes)
|
||||
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this))
|
||||
super.visit(parser)
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,11 +90,13 @@ sealed class QueryCriteria {
|
||||
data class FungibleAssetQueryCriteria @JvmOverloads constructor(val participants: List<AbstractParty>? = null,
|
||||
val owner: List<AbstractParty>? = null,
|
||||
val quantity: ColumnPredicate<Long>? = null,
|
||||
val issuerPartyName: List<AbstractParty>? = null,
|
||||
val issuer: List<AbstractParty>? = null,
|
||||
val issuerRef: List<OpaqueBytes>? = null,
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() {
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null) : CommonQueryCriteria() {
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this))
|
||||
super.visit(parser)
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
}
|
||||
|
||||
@ -101,9 +112,11 @@ sealed class QueryCriteria {
|
||||
*/
|
||||
data class VaultCustomQueryCriteria<L : PersistentState> @JvmOverloads constructor
|
||||
(val expression: CriteriaExpression<L, Boolean>,
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() {
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null) : CommonQueryCriteria() {
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this))
|
||||
super.visit(parser)
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,39 +9,36 @@ import kotlin.reflect.KProperty1
|
||||
import kotlin.reflect.jvm.javaGetter
|
||||
|
||||
@CordaSerializable
|
||||
enum class BinaryLogicalOperator {
|
||||
interface Operator
|
||||
|
||||
enum class BinaryLogicalOperator : Operator {
|
||||
AND,
|
||||
OR
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class EqualityComparisonOperator {
|
||||
enum class EqualityComparisonOperator : Operator {
|
||||
EQUAL,
|
||||
NOT_EQUAL
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class BinaryComparisonOperator {
|
||||
enum class BinaryComparisonOperator : Operator {
|
||||
LESS_THAN,
|
||||
LESS_THAN_OR_EQUAL,
|
||||
GREATER_THAN,
|
||||
GREATER_THAN_OR_EQUAL,
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class NullOperator {
|
||||
enum class NullOperator : Operator {
|
||||
IS_NULL,
|
||||
NOT_NULL
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class LikenessOperator {
|
||||
enum class LikenessOperator : Operator {
|
||||
LIKE,
|
||||
NOT_LIKE
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class CollectionOperator {
|
||||
enum class CollectionOperator : Operator {
|
||||
IN,
|
||||
NOT_IN
|
||||
}
|
||||
@ -151,7 +148,7 @@ data class Sort(val columns: Collection<SortColumn>) {
|
||||
|
||||
enum class VaultStateAttribute(val attributeName: String) : Attribute {
|
||||
/** Vault States */
|
||||
NOTARY_NAME("notaryName"),
|
||||
NOTARY_NAME("notary"),
|
||||
CONTRACT_TYPE("contractStateClassName"),
|
||||
STATE_STATUS("stateStatus"),
|
||||
RECORDED_TIME("recordedTime"),
|
||||
|
@ -17,10 +17,17 @@ object CommonSchema
|
||||
/**
|
||||
* First version of the Vault ORM schema
|
||||
*/
|
||||
object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, version = 1, mappedTypes = listOf(Party::class.java)) {
|
||||
object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, version = 1, mappedTypes = emptyList()) {
|
||||
|
||||
@MappedSuperclass
|
||||
open class LinearState(
|
||||
/** [ContractState] attributes */
|
||||
|
||||
/** X500Name of participant parties **/
|
||||
@ElementCollection
|
||||
@Column(name = "participants")
|
||||
var participants: MutableSet<AbstractParty>? = null,
|
||||
|
||||
/**
|
||||
* Represents a [LinearState] [UniqueIdentifier]
|
||||
*/
|
||||
@ -31,18 +38,26 @@ object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, vers
|
||||
var uuid: UUID
|
||||
|
||||
) : PersistentState() {
|
||||
constructor(uid: UniqueIdentifier) : this(externalId = uid.externalId, uuid = uid.id)
|
||||
constructor(uid: UniqueIdentifier, _participants: Set<AbstractParty>)
|
||||
: this(participants = _participants.toMutableSet(),
|
||||
externalId = uid.externalId,
|
||||
uuid = uid.id)
|
||||
}
|
||||
|
||||
@MappedSuperclass
|
||||
open class FungibleState(
|
||||
/** [ContractState] attributes */
|
||||
@OneToMany(cascade = arrayOf(CascadeType.ALL))
|
||||
var participants: Set<CommonSchemaV1.Party>,
|
||||
|
||||
/** X500Name of participant parties **/
|
||||
@ElementCollection
|
||||
@Column(name = "participants")
|
||||
var participants: MutableSet<AbstractParty>? = null,
|
||||
|
||||
/** [OwnableState] attributes */
|
||||
@OneToOne(cascade = arrayOf(CascadeType.ALL))
|
||||
var ownerKey: CommonSchemaV1.Party,
|
||||
|
||||
/** X500Name of owner party **/
|
||||
@Column(name = "owner_name")
|
||||
var owner: AbstractParty,
|
||||
|
||||
/** [FungibleAsset] attributes
|
||||
*
|
||||
@ -55,42 +70,12 @@ object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, vers
|
||||
var quantity: Long,
|
||||
|
||||
/** Issuer attributes */
|
||||
@OneToOne(cascade = arrayOf(CascadeType.ALL))
|
||||
var issuerParty: CommonSchemaV1.Party,
|
||||
|
||||
/** X500Name of issuer party **/
|
||||
@Column(name = "issuer_name")
|
||||
var issuer: AbstractParty,
|
||||
|
||||
@Column(name = "issuer_reference")
|
||||
var issuerRef: ByteArray
|
||||
) : PersistentState() {
|
||||
constructor(_participants: Set<AbstractParty>, _ownerKey: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: ByteArray)
|
||||
: this(participants = _participants.map { CommonSchemaV1.Party(it) }.toSet(),
|
||||
ownerKey = CommonSchemaV1.Party(_ownerKey),
|
||||
quantity = _quantity,
|
||||
issuerParty = CommonSchemaV1.Party(_issuerParty),
|
||||
issuerRef = _issuerRef)
|
||||
}
|
||||
|
||||
/**
|
||||
* Party entity (to be replaced by referencing final Identity Schema)
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "vault_party",
|
||||
indexes = arrayOf(Index(name = "party_name_idx", columnList = "party_name")))
|
||||
class Party(
|
||||
@Id
|
||||
@GeneratedValue
|
||||
@Column(name = "party_id")
|
||||
var id: Int,
|
||||
|
||||
/**
|
||||
* [Party] attributes
|
||||
*/
|
||||
@Column(name = "party_name")
|
||||
var name: String,
|
||||
|
||||
@Column(name = "party_key", length = 65535) // TODO What is the upper limit on size of CompositeKey?)
|
||||
var key: String
|
||||
) {
|
||||
constructor(party: AbstractParty)
|
||||
: this(0, party.nameOrNull()?.toString() ?: party.toString(), party.owningKey.toBase58String())
|
||||
}
|
||||
) : PersistentState()
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package net.corda.core.schemas
|
||||
|
||||
import io.requery.Persistable
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
@ -69,4 +68,4 @@ data class PersistentStateRef(
|
||||
/**
|
||||
* Marker interface to denote a persistable Corda state entity that will always have a transaction id and index
|
||||
*/
|
||||
interface StatePersistable : Persistable
|
||||
interface StatePersistable
|
@ -2,6 +2,7 @@ package net.corda.core.schemas.converters
|
||||
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import javax.persistence.AttributeConverter
|
||||
import javax.persistence.Converter
|
||||
@ -17,9 +18,15 @@ class AbstractPartyToX500NameAsStringConverter(identitySvc: () -> IdentityServic
|
||||
identitySvc()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val log = loggerFor<AbstractPartyToX500NameAsStringConverter>()
|
||||
}
|
||||
|
||||
override fun convertToDatabaseColumn(party: AbstractParty?): String? {
|
||||
party?.let {
|
||||
return identityService.partyFromAnonymous(party)?.toString()
|
||||
val partyName = identityService.partyFromAnonymous(party)?.toString()
|
||||
if (partyName != null) return partyName
|
||||
else log.warn ("Identity service unable to resolve AbstractParty: $party")
|
||||
}
|
||||
return null // non resolvable anonymous parties
|
||||
}
|
||||
@ -27,7 +34,8 @@ class AbstractPartyToX500NameAsStringConverter(identitySvc: () -> IdentityServic
|
||||
override fun convertToEntityAttribute(dbData: String?): AbstractParty? {
|
||||
dbData?.let {
|
||||
val party = identityService.partyFromX500Name(X500Name(dbData))
|
||||
return party as AbstractParty
|
||||
if (party != null) return party
|
||||
else log.warn ("Identity service unable to resolve X500name: $dbData")
|
||||
}
|
||||
return null // non resolvable anonymous parties are stored as nulls
|
||||
}
|
||||
|
@ -1,27 +0,0 @@
|
||||
package net.corda.core.schemas.requery
|
||||
|
||||
import io.requery.Key
|
||||
import io.requery.Persistable
|
||||
import io.requery.Superclass
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.schemas.StatePersistable
|
||||
|
||||
import javax.persistence.Column
|
||||
|
||||
object Requery {
|
||||
/**
|
||||
* A super class for all mapped states exported to a schema that ensures the [StateRef] appears on the database row. The
|
||||
* [StateRef] will be set to the correct value by the framework (there's no need to set during mapping generation by the state itself).
|
||||
*/
|
||||
// TODO: this interface will supercede the existing [PersistentState] interface defined in PersistentTypes.kt
|
||||
// once we cut-over all existing Hibernate ContractState persistence to Requery
|
||||
@Superclass interface PersistentState : StatePersistable {
|
||||
@get:Key
|
||||
@get:Column(name = "transaction_id", length = 64)
|
||||
var txId: String
|
||||
|
||||
@get:Key
|
||||
@get:Column(name = "output_index")
|
||||
var index: Int
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package net.corda.core.schemas.requery.converters
|
||||
|
||||
import io.requery.Converter
|
||||
import java.sql.Blob
|
||||
import javax.sql.rowset.serial.SerialBlob
|
||||
|
||||
/**
|
||||
* Converts from a [ByteArray] to a [Blob].
|
||||
*/
|
||||
class BlobConverter : Converter<ByteArray, Blob> {
|
||||
|
||||
override fun getMappedType(): Class<ByteArray> = ByteArray::class.java
|
||||
|
||||
override fun getPersistedType(): Class<Blob> = Blob::class.java
|
||||
|
||||
/**
|
||||
* creates BLOB(INT.MAX) = 2 GB
|
||||
*/
|
||||
override fun getPersistedSize(): Int? = null
|
||||
|
||||
override fun convertToPersisted(value: ByteArray?): Blob? {
|
||||
return value?.let { SerialBlob(value) }
|
||||
}
|
||||
|
||||
override fun convertToMapped(type: Class<out ByteArray>?, value: Blob?): ByteArray? {
|
||||
return value?.getBytes(1, value.length().toInt())
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package net.corda.core.schemas.requery.converters
|
||||
|
||||
import io.requery.Converter
|
||||
import java.sql.Timestamp
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Converts from a [Instant] to a [java.sql.Timestamp] for Java 8. Note that
|
||||
* when converting between the time type and the database type all times will be converted to the
|
||||
* UTC zone offset.
|
||||
*/
|
||||
class InstantConverter : Converter<Instant, Timestamp> {
|
||||
override fun getMappedType() = Instant::class.java
|
||||
|
||||
override fun getPersistedType() = Timestamp::class.java
|
||||
|
||||
override fun getPersistedSize() = null
|
||||
|
||||
override fun convertToPersisted(value: Instant?) = value?.let { Timestamp.from(it) }
|
||||
|
||||
override fun convertToMapped(type: Class<out Instant>, value: Timestamp?) = value?.toInstant()
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
package net.corda.core.schemas.requery.converters
|
||||
|
||||
import io.requery.Converter
|
||||
import net.corda.core.crypto.SecureHash
|
||||
|
||||
/**
|
||||
* Convert from a [SecureHash] to a [String]
|
||||
*/
|
||||
class SecureHashConverter : Converter<SecureHash, String> {
|
||||
|
||||
override fun getMappedType(): Class<SecureHash> = SecureHash::class.java
|
||||
|
||||
override fun getPersistedType(): Class<String> = String::class.java
|
||||
|
||||
/**
|
||||
* SecureHash consists of 32 bytes which need VARCHAR(64) in hex
|
||||
* TODO: think about other hash widths
|
||||
*/
|
||||
override fun getPersistedSize(): Int? = 64
|
||||
|
||||
override fun convertToPersisted(value: SecureHash?): String? {
|
||||
return value?.toString()
|
||||
}
|
||||
|
||||
override fun convertToMapped(type: Class<out SecureHash>, value: String?): SecureHash? {
|
||||
return value?.let { SecureHash.parse(value) }
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package net.corda.core.schemas.requery.converters
|
||||
|
||||
import io.requery.Converter
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.SecureHash
|
||||
|
||||
/**
|
||||
* Converts from a [StateRef] to a Composite Key defined by a [String] txnHash and an [Int] index
|
||||
*/
|
||||
class StateRefConverter : Converter<StateRef, Pair<String, Int>> {
|
||||
override fun getMappedType() = StateRef::class.java
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun getPersistedType() = Pair::class.java as Class<Pair<String, Int>>
|
||||
|
||||
override fun getPersistedSize() = null
|
||||
|
||||
override fun convertToPersisted(value: StateRef?) = value?.let { Pair(it.txhash.toString(), it.index) }
|
||||
|
||||
override fun convertToMapped(type: Class<out StateRef>, value: Pair<String, Int>?) = value?.let { StateRef(SecureHash.parse(it.first), it.second) }
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package net.corda.core.schemas.requery.converters
|
||||
|
||||
import io.requery.converter.EnumOrdinalConverter
|
||||
import net.corda.core.node.services.Vault
|
||||
|
||||
/**
|
||||
* Converter which persists a [Vault.StateStatus] enum using its enum ordinal representation
|
||||
*/
|
||||
class VaultStateStatusConverter : EnumOrdinalConverter<Vault.StateStatus>(Vault.StateStatus::class.java)
|
@ -0,0 +1,14 @@
|
||||
package net.corda.core.serialization
|
||||
|
||||
/**
|
||||
* This annotation is a marker to indicate which secondary constructors should be considered, and in which
|
||||
* order, for evolving objects during their deserialisation.
|
||||
*
|
||||
* Versions will be considered in descending order, currently duplicate versions will result in
|
||||
* non deterministic behaviour when deserialising objects
|
||||
*/
|
||||
@Target(AnnotationTarget.CONSTRUCTOR)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class DeprecatedConstructorForDeserialization(val version: Int)
|
||||
|
||||
|
@ -39,7 +39,7 @@ interface SerializationContext {
|
||||
/**
|
||||
* When serializing, use the format this header sequence represents.
|
||||
*/
|
||||
val preferedSerializationVersion: ByteSequence
|
||||
val preferredSerializationVersion: ByteSequence
|
||||
/**
|
||||
* The class loader to use for deserialization.
|
||||
*/
|
||||
|
@ -8,6 +8,7 @@ import net.corda.core.crypto.keys
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.util.function.Predicate
|
||||
@ -17,6 +18,7 @@ import java.util.function.Predicate
|
||||
* by a [SignedTransaction] that carries the signatures over this payload.
|
||||
* The identity of the transaction is the Merkle tree root of its components (see [MerkleTree]).
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class WireTransaction(
|
||||
/** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */
|
||||
override val inputs: List<StateRef>,
|
||||
|
@ -139,7 +139,10 @@ fun ByteArray.sequence(offset: Int = 0, size: Int = this.size) = ByteSequence.of
|
||||
fun ByteArray.toHexString(): String = DatatypeConverter.printHexBinary(this)
|
||||
fun String.parseAsHex(): ByteArray = DatatypeConverter.parseHexBinary(this)
|
||||
|
||||
private class OpaqueBytesSubSequence(override val bytes: ByteArray, override val offset: Int, override val size: Int) : ByteSequence() {
|
||||
/**
|
||||
* Class is public for serialization purposes
|
||||
*/
|
||||
class OpaqueBytesSubSequence(override val bytes: ByteArray, override val offset: Int, override val size: Int) : ByteSequence() {
|
||||
init {
|
||||
require(offset >= 0 && offset < bytes.size)
|
||||
require(size >= 0 && size <= bytes.size)
|
||||
|
@ -43,31 +43,31 @@ inline fun Logger.debug(msg: () -> String) {
|
||||
* Extension method for easier construction of [Duration]s in terms of integer days: `val twoDays = 2.days`.
|
||||
* @see Duration.ofDays
|
||||
*/
|
||||
inline val Int.days: Duration get() = Duration.ofDays(toLong())
|
||||
val Int.days: Duration get() = Duration.ofDays(toLong())
|
||||
|
||||
/**
|
||||
* Extension method for easier construction of [Duration]s in terms of integer hours: `val twoHours = 2.hours`.
|
||||
* @see Duration.ofHours
|
||||
*/
|
||||
inline val Int.hours: Duration get() = Duration.ofHours(toLong())
|
||||
val Int.hours: Duration get() = Duration.ofHours(toLong())
|
||||
|
||||
/**
|
||||
* Extension method for easier construction of [Duration]s in terms of integer minutes: `val twoMinutes = 2.minutes`.
|
||||
* @see Duration.ofMinutes
|
||||
*/
|
||||
inline val Int.minutes: Duration get() = Duration.ofMinutes(toLong())
|
||||
val Int.minutes: Duration get() = Duration.ofMinutes(toLong())
|
||||
|
||||
/**
|
||||
* Extension method for easier construction of [Duration]s in terms of integer seconds: `val twoSeconds = 2.seconds`.
|
||||
* @see Duration.ofSeconds
|
||||
*/
|
||||
inline val Int.seconds: Duration get() = Duration.ofSeconds(toLong())
|
||||
val Int.seconds: Duration get() = Duration.ofSeconds(toLong())
|
||||
|
||||
/**
|
||||
* Extension method for easier construction of [Duration]s in terms of integer milliseconds: `val twoMillis = 2.millis`.
|
||||
* @see Duration.ofMillis
|
||||
*/
|
||||
inline val Int.millis: Duration get() = Duration.ofMillis(toLong())
|
||||
val Int.millis: Duration get() = Duration.ofMillis(toLong())
|
||||
|
||||
/**
|
||||
* A simple wrapper that enables the use of Kotlin's `val x by transient { ... }` syntax. Such a property
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.core.utilities
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.net.URI
|
||||
|
||||
/**
|
||||
@ -7,6 +8,7 @@ import java.net.URI
|
||||
* @param host a hostname or IP address. IPv6 addresses must not be enclosed in square brackets.
|
||||
* @param port a valid port number.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class NetworkHostAndPort(val host: String, val port: Int) {
|
||||
companion object {
|
||||
internal val invalidPortFormat = "Invalid port: %s"
|
||||
|
@ -1,5 +1,7 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.finance.*
|
||||
import net.corda.core.contracts.Amount.Companion.sumOrZero
|
||||
import org.junit.Test
|
||||
import java.math.BigDecimal
|
||||
import java.util.*
|
||||
@ -13,13 +15,6 @@ import kotlin.test.assertTrue
|
||||
* Tests of the [Amount] class.
|
||||
*/
|
||||
class AmountTests {
|
||||
@Test
|
||||
fun basicCurrency() {
|
||||
val expected = 1000L
|
||||
val amount = Amount(expected, GBP)
|
||||
assertEquals(expected, amount.quantity)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `make sure Amount has decimal places`() {
|
||||
val x = Amount(1, Currency.getInstance("USD"))
|
||||
@ -27,7 +22,7 @@ class AmountTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decimalConversion() {
|
||||
fun `decimal conversion`() {
|
||||
val quantity = 1234L
|
||||
val amountGBP = Amount(quantity, GBP)
|
||||
val expectedGBP = BigDecimal("12.34")
|
||||
@ -49,22 +44,6 @@ class AmountTests {
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parsing() {
|
||||
assertEquals(Amount(1234L, GBP), Amount.parseCurrency("£12.34"))
|
||||
assertEquals(Amount(1200L, GBP), Amount.parseCurrency("£12"))
|
||||
assertEquals(Amount(1000L, USD), Amount.parseCurrency("$10"))
|
||||
assertEquals(Amount(5000L, JPY), Amount.parseCurrency("¥5000"))
|
||||
assertEquals(Amount(500000L, RUB), Amount.parseCurrency("₽5000"))
|
||||
assertEquals(Amount(1500000000L, CHF), Amount.parseCurrency("15,000,000 CHF"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rendering() {
|
||||
assertEquals("5000 JPY", Amount.parseCurrency("¥5000").toString())
|
||||
assertEquals("50.12 USD", Amount.parseCurrency("$50.12").toString())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun split() {
|
||||
for (baseQuantity in 0..1000) {
|
||||
@ -81,7 +60,7 @@ class AmountTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun amountTransfersEquality() {
|
||||
fun `amount transfers equality`() {
|
||||
val partyA = "A"
|
||||
val partyB = "B"
|
||||
val partyC = "C"
|
||||
@ -106,7 +85,7 @@ class AmountTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun amountTransferAggregation() {
|
||||
fun `amount transfer aggregation`() {
|
||||
val partyA = "A"
|
||||
val partyB = "B"
|
||||
val partyC = "C"
|
||||
@ -137,7 +116,7 @@ class AmountTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun amountTransferApply() {
|
||||
fun `amount transfer apply`() {
|
||||
val partyA = "A"
|
||||
val partyB = "B"
|
||||
val partyC = "C"
|
||||
@ -182,6 +161,5 @@ class AmountTests {
|
||||
assertEquals(originalTotals[Pair(partyC, USD)], newTotals3[Pair(partyC, USD)])
|
||||
assertEquals(originalTotals[Pair(partyA, GBP)], newTotals3[Pair(partyA, GBP)])
|
||||
assertEquals(originalTotals[Pair(partyB, GBP)], newTotals3[Pair(partyB, GBP)])
|
||||
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.TestDependencyInjectionBase
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.dummyCommand
|
||||
import net.corda.testing.node.MockServices
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -27,6 +28,7 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() {
|
||||
interface Commands {
|
||||
data class Cmd1(val id: Int) : CommandData, Commands
|
||||
data class Cmd2(val id: Int) : CommandData, Commands
|
||||
data class Cmd3(val id: Int) : CommandData, Commands // Unused command, required for command not-present checks.
|
||||
}
|
||||
|
||||
|
||||
@ -50,7 +52,11 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() {
|
||||
|
||||
private fun makeDummyStateAndRef(data: Any): StateAndRef<*> {
|
||||
val dummyState = makeDummyState(data)
|
||||
val fakeIssueTx = services.signInitialTransaction(TransactionBuilder(notary = DUMMY_NOTARY).addOutputState(dummyState))
|
||||
val fakeIssueTx = services.signInitialTransaction(
|
||||
TransactionBuilder(notary = DUMMY_NOTARY)
|
||||
.addOutputState(dummyState)
|
||||
.addCommand(dummyCommand())
|
||||
)
|
||||
services.recordTransactions(fakeIssueTx)
|
||||
val dummyStateRef = StateRef(fakeIssueTx.id, 0)
|
||||
return StateAndRef(TransactionState(dummyState, DUMMY_NOTARY, null), dummyStateRef)
|
||||
@ -182,7 +188,7 @@ class LedgerTransactionQueryTests : TestDependencyInjectionBase() {
|
||||
val intCmd2 = ltx.commandsOfType<Commands.Cmd2>()
|
||||
assertEquals(5, intCmd2.size)
|
||||
assertEquals(listOf(0, 1, 2, 3, 4), intCmd2.map { it.value.id })
|
||||
val notPresentQuery = ltx.commandsOfType(FungibleAsset.Commands.Exit::class.java)
|
||||
val notPresentQuery = ltx.commandsOfType(Commands.Cmd3::class.java)
|
||||
assertEquals(emptyList(), notPresentQuery)
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.`issued by`
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.testing.MEGA_CORP
|
||||
import net.corda.testing.MINI_CORP
|
||||
import net.corda.testing.ledger
|
||||
@ -27,7 +28,6 @@ class TransactionEncumbranceTests {
|
||||
val timeLock = DummyTimeLock.State(FIVE_PM)
|
||||
|
||||
class DummyTimeLock : Contract {
|
||||
override val legalContractReference = SecureHash.sha256("DummyTimeLock")
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
val timeLockInput = tx.inputsOfType<State>().singleOrNull() ?: return
|
||||
val time = tx.timeWindow?.untilTime ?: throw IllegalArgumentException("Transactions containing time-locks must have a time-window")
|
||||
|
@ -34,14 +34,15 @@ class TransactionGraphSearchTests : TestDependencyInjectionBase() {
|
||||
val notaryServices = MockServices(DUMMY_NOTARY_KEY)
|
||||
|
||||
val originBuilder = TransactionBuilder(DUMMY_NOTARY)
|
||||
originBuilder.addOutputState(DummyState(random31BitValue()))
|
||||
originBuilder.addCommand(command, MEGA_CORP_PUBKEY)
|
||||
.addOutputState(DummyState(random31BitValue()))
|
||||
.addCommand(command, MEGA_CORP_PUBKEY)
|
||||
|
||||
val originPtx = megaCorpServices.signInitialTransaction(originBuilder)
|
||||
val originTx = notaryServices.addSignature(originPtx)
|
||||
|
||||
val inputBuilder = TransactionBuilder(DUMMY_NOTARY)
|
||||
inputBuilder.addInputState(originTx.tx.outRef<DummyState>(0))
|
||||
.addInputState(originTx.tx.outRef<DummyState>(0))
|
||||
.addCommand(dummyCommand(MEGA_CORP_PUBKEY))
|
||||
|
||||
val inputPtx = megaCorpServices.signInitialTransaction(inputBuilder)
|
||||
val inputTx = megaCorpServices.addSignature(inputPtx)
|
||||
|
@ -1,12 +1,12 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.composite.CompositeKey
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.finance.contracts.asset.DUMMY_CASH_ISSUER_KEY
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import org.junit.Test
|
||||
|
@ -25,16 +25,16 @@ class CompositeKeyTests : TestDependencyInjectionBase() {
|
||||
@JvmField
|
||||
val tempFolder: TemporaryFolder = TemporaryFolder()
|
||||
|
||||
val aliceKey = generateKeyPair()
|
||||
val bobKey = generateKeyPair()
|
||||
val charlieKey = generateKeyPair()
|
||||
private val aliceKey = generateKeyPair()
|
||||
private val bobKey = generateKeyPair()
|
||||
private val charlieKey = generateKeyPair()
|
||||
|
||||
val alicePublicKey: PublicKey = aliceKey.public
|
||||
val bobPublicKey: PublicKey = bobKey.public
|
||||
val charliePublicKey: PublicKey = charlieKey.public
|
||||
private val alicePublicKey: PublicKey = aliceKey.public
|
||||
private val bobPublicKey: PublicKey = bobKey.public
|
||||
private val charliePublicKey: PublicKey = charlieKey.public
|
||||
|
||||
val message = OpaqueBytes("Transaction".toByteArray())
|
||||
val secureHash = message.sha256()
|
||||
private val message = OpaqueBytes("Transaction".toByteArray())
|
||||
private val secureHash = message.sha256()
|
||||
|
||||
// By lazy is required so that the serialisers are configured before vals initialisation takes place (they internally invoke serialise).
|
||||
val aliceSignature by lazy { aliceKey.sign(SignableData(secureHash, SignatureMetadata(1, Crypto.findSignatureScheme(alicePublicKey).schemeNumberID))) }
|
||||
@ -43,7 +43,6 @@ class CompositeKeyTests : TestDependencyInjectionBase() {
|
||||
|
||||
@Test
|
||||
fun `(Alice) fulfilled by Alice signature`() {
|
||||
println(aliceKey.serialize().hash)
|
||||
assertTrue { alicePublicKey.isFulfilledBy(aliceSignature.by) }
|
||||
assertFalse { alicePublicKey.isFulfilledBy(charlieSignature.by) }
|
||||
}
|
||||
|
@ -343,7 +343,7 @@ class CryptoUtilsTest {
|
||||
// test list of supported algorithms
|
||||
@Test
|
||||
fun `Check supported algorithms`() {
|
||||
val algList: List<String> = Crypto.supportedSignatureSchemes.keys.toList()
|
||||
val algList: List<String> = Crypto.supportedSignatureSchemes().map { it.schemeCodeName }
|
||||
val expectedAlgSet = setOf("RSA_SHA256", "ECDSA_SECP256K1_SHA256", "ECDSA_SECP256R1_SHA256", "EDDSA_ED25519_SHA512", "SPHINCS-256_SHA512", "COMPOSITE")
|
||||
assertTrue { Sets.symmetricDifference(expectedAlgSet, algList.toSet()).isEmpty(); }
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash.Companion.zeroHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.`issued by`
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.testing.*
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
|
@ -4,20 +4,18 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.messaging.SingleMessageRecipient
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FetchAttachmentsFlow
|
||||
import net.corda.core.internal.FetchDataFlow
|
||||
import net.corda.core.messaging.SingleMessageRecipient
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.database.RequeryConfiguration
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.persistence.schemas.requery.AttachmentEntity
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.node.utilities.DatabaseTransactionManager
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
import net.corda.testing.node.makeTestDatabaseProperties
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -32,13 +30,10 @@ import kotlin.test.assertFailsWith
|
||||
|
||||
class AttachmentTests {
|
||||
lateinit var mockNet: MockNetwork
|
||||
lateinit var configuration: RequeryConfiguration
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockNet = MockNetwork()
|
||||
val dataSourceProperties = makeTestDataSourceProperties()
|
||||
configuration = RequeryConfiguration(dataSourceProperties, databaseProperties = makeTestDatabaseProperties())
|
||||
}
|
||||
|
||||
@After
|
||||
@ -137,11 +132,9 @@ class AttachmentTests {
|
||||
val corruptBytes = "arggghhhh".toByteArray()
|
||||
System.arraycopy(corruptBytes, 0, attachment, 0, corruptBytes.size)
|
||||
|
||||
val corruptAttachment = AttachmentEntity()
|
||||
corruptAttachment.attId = id
|
||||
corruptAttachment.content = attachment
|
||||
val corruptAttachment = NodeAttachmentService.DBAttachment(attId = id.toString(), content = attachment)
|
||||
n0.database.transaction {
|
||||
n0.attachments.session.update(corruptAttachment)
|
||||
DatabaseTransactionManager.current().session.update(corruptAttachment)
|
||||
}
|
||||
|
||||
// Get n1 to fetch the attachment. Should receive corrupted bytes.
|
||||
|
@ -3,13 +3,14 @@ package net.corda.core.flows
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.requireThat
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.testing.MINI_CORP_KEY
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockServices
|
||||
import org.junit.After
|
||||
@ -77,16 +78,18 @@ class CollectSignaturesFlowTests {
|
||||
}
|
||||
|
||||
@InitiatedBy(TestFlow.Initiator::class)
|
||||
class Responder(val otherParty: Party) : FlowLogic<SignedTransaction>() {
|
||||
class Responder(val otherParty: Party, val identities: Map<Party, AnonymousParty>) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val state = receive<DummyContract.MultiOwnerState>(otherParty).unwrap { it }
|
||||
val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity
|
||||
|
||||
val command = Command(DummyContract.Commands.Create(), state.participants.map { it.owningKey })
|
||||
val myInputKeys = state.participants.map { it.owningKey }
|
||||
val myKeys = myInputKeys + (identities[serviceHub.myInfo.legalIdentity] ?: serviceHub.myInfo.legalIdentity).owningKey
|
||||
val command = Command(DummyContract.Commands.Create(), myInputKeys)
|
||||
val builder = TransactionBuilder(notary).withItems(state, command)
|
||||
val ptx = serviceHub.signInitialTransaction(builder)
|
||||
val stx = subFlow(CollectSignaturesFlow(ptx))
|
||||
val stx = subFlow(CollectSignaturesFlow(ptx, myKeys))
|
||||
val ftx = subFlow(FinalityFlow(stx)).single()
|
||||
|
||||
return ftx
|
||||
@ -103,10 +106,11 @@ class CollectSignaturesFlowTests {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity
|
||||
val command = Command(DummyContract.Commands.Create(), state.participants.map { it.owningKey })
|
||||
val myInputKeys = state.participants.map { it.owningKey }
|
||||
val command = Command(DummyContract.Commands.Create(), myInputKeys)
|
||||
val builder = TransactionBuilder(notary).withItems(state, command)
|
||||
val ptx = serviceHub.signInitialTransaction(builder)
|
||||
val stx = subFlow(CollectSignaturesFlow(ptx))
|
||||
val stx = subFlow(CollectSignaturesFlow(ptx, myInputKeys))
|
||||
val ftx = subFlow(FinalityFlow(stx)).single()
|
||||
|
||||
return ftx
|
||||
@ -136,9 +140,12 @@ class CollectSignaturesFlowTests {
|
||||
|
||||
@Test
|
||||
fun `successfully collects two signatures`() {
|
||||
val bConfidentialIdentity = b.services.keyManagementService.freshKeyAndCert(b.info.legalIdentityAndCert, false)
|
||||
// Normally this is handled by TransactionKeyFlow, but here we have to manually let A know about the identity
|
||||
a.services.identityService.verifyAndRegisterIdentity(bConfidentialIdentity)
|
||||
registerFlowOnAllNodes(TestFlowTwo.Responder::class)
|
||||
val magicNumber = 1337
|
||||
val parties = listOf(a.info.legalIdentity, b.info.legalIdentity, c.info.legalIdentity)
|
||||
val parties = listOf(a.info.legalIdentity, bConfidentialIdentity.party, c.info.legalIdentity)
|
||||
val state = DummyContract.MultiOwnerState(magicNumber, parties)
|
||||
val flow = a.services.startFlow(TestFlowTwo.Initiator(state))
|
||||
mockNet.runNetwork()
|
||||
|
@ -1,22 +1,23 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.services.queryBy
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.internal.Emoji
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.finance.USD
|
||||
import net.corda.finance.`issued by`
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.flows.CashIssueFlow
|
||||
import net.corda.node.internal.CordaRPCOpsImpl
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.FlowPermissions.Companion.startFlowPermission
|
||||
import net.corda.nodeapi.User
|
||||
import net.corda.testing.RPCDriverExposedDSLInterface
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
@ -42,11 +43,14 @@ class ContractUpgradeFlowTest {
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = MockNetwork()
|
||||
val nodes = mockNet.createSomeNodes()
|
||||
val nodes = mockNet.createSomeNodes(notaryKeyPair = null) // prevent generation of notary override
|
||||
a = nodes.partyNodes[0]
|
||||
b = nodes.partyNodes[1]
|
||||
notary = nodes.notaryNode.info.notaryIdentity
|
||||
mockNet.runNetwork()
|
||||
|
||||
val nodeIdentity = nodes.notaryNode.info.legalIdentitiesAndCerts.single { it.party == nodes.notaryNode.info.notaryIdentity }
|
||||
a.services.identityService.verifyAndRegisterIdentity(nodeIdentity)
|
||||
b.services.identityService.verifyAndRegisterIdentity(nodeIdentity)
|
||||
}
|
||||
|
||||
@After
|
||||
@ -173,8 +177,7 @@ class ContractUpgradeFlowTest {
|
||||
@Test
|
||||
fun `upgrade Cash to v2`() {
|
||||
// Create some cash.
|
||||
val anonymous = false
|
||||
val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), a.info.legalIdentity, notary, anonymous)).resultFuture
|
||||
val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), notary)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
val stx = result.getOrThrow().stx
|
||||
val stateAndRef = stx.tx.outRef<Cash.State>(0)
|
||||
@ -200,7 +203,7 @@ class ContractUpgradeFlowTest {
|
||||
override val contract = CashV2()
|
||||
override val participants = owners
|
||||
|
||||
override fun move(newAmount: Amount<Issued<Currency>>, newOwner: AbstractParty) = copy(amount = amount.copy(newAmount.quantity), owners = listOf(newOwner))
|
||||
override fun withNewOwnerAndAmount(newAmount: Amount<Issued<Currency>>, newOwner: AbstractParty) = copy(amount = amount.copy(newAmount.quantity), owners = listOf(newOwner))
|
||||
override fun toString() = "${Emoji.bagOfCash}New Cash($amount at ${amount.token.issuer} owned by $owner)"
|
||||
override fun withNewOwner(newOwner: AbstractParty) = CommandAndState(Cash.Commands.Move(), copy(owners = listOf(newOwner)))
|
||||
}
|
||||
@ -208,9 +211,6 @@ class ContractUpgradeFlowTest {
|
||||
override fun upgrade(state: Cash.State) = CashV2.State(state.amount.times(1000), listOf(state.owner))
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {}
|
||||
|
||||
// Dummy Cash contract for testing.
|
||||
override val legalContractReference = SecureHash.sha256("")
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
|
@ -1,12 +1,12 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.GBP
|
||||
import net.corda.core.contracts.Issued
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.GBP
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockServices
|
||||
import org.junit.After
|
||||
|
@ -0,0 +1,87 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.finance.DOLLARS
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.finance.flows.CashIssueAndPaymentFlow
|
||||
import net.corda.testing.ALICE
|
||||
import net.corda.testing.BOB
|
||||
import net.corda.testing.DUMMY_NOTARY
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class IdentitySyncFlowTests {
|
||||
lateinit var mockNet: MockNetwork
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
// We run this in parallel threads to help catch any race conditions that may exist.
|
||||
mockNet = MockNetwork(networkSendManuallyPumped = false, threadPerNode = true)
|
||||
}
|
||||
|
||||
@After
|
||||
fun cleanUp() {
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sync confidential identities`() {
|
||||
// Set up values we'll need
|
||||
val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
|
||||
val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name)
|
||||
val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name)
|
||||
val alice: Party = aliceNode.services.myInfo.legalIdentity
|
||||
val bob: Party = bobNode.services.myInfo.legalIdentity
|
||||
aliceNode.services.identityService.verifyAndRegisterIdentity(bobNode.info.legalIdentityAndCert)
|
||||
aliceNode.services.identityService.verifyAndRegisterIdentity(notaryNode.info.legalIdentityAndCert)
|
||||
bobNode.services.identityService.verifyAndRegisterIdentity(aliceNode.info.legalIdentityAndCert)
|
||||
bobNode.services.identityService.verifyAndRegisterIdentity(notaryNode.info.legalIdentityAndCert)
|
||||
bobNode.registerInitiatedFlow(Receive::class.java)
|
||||
|
||||
// Alice issues then pays some cash to a new confidential identity that Bob doesn't know about
|
||||
val anonymous = true
|
||||
val ref = OpaqueBytes.of(0x01)
|
||||
val issueFlow = aliceNode.services.startFlow(CashIssueAndPaymentFlow(1000.DOLLARS, ref, alice, anonymous, notaryNode.services.myInfo.notaryIdentity))
|
||||
val issueTx = issueFlow.resultFuture.getOrThrow().stx
|
||||
val confidentialIdentity = issueTx.tx.outputs.map { it.data }.filterIsInstance<Cash.State>().single().owner
|
||||
assertNull(bobNode.services.identityService.partyFromAnonymous(confidentialIdentity))
|
||||
|
||||
// Run the flow to sync up the identities
|
||||
aliceNode.services.startFlow(Initiator(bob, issueTx.tx)).resultFuture.getOrThrow()
|
||||
val expected = aliceNode.services.identityService.partyFromAnonymous(confidentialIdentity)
|
||||
val actual = bobNode.services.identityService.partyFromAnonymous(confidentialIdentity)
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
/**
|
||||
* Very lightweight wrapping flow to trigger the counterparty flow that receives the identities.
|
||||
*/
|
||||
@InitiatingFlow
|
||||
class Initiator(val otherSide: Party, val tx: WireTransaction): FlowLogic<Boolean>() {
|
||||
@Suspendable
|
||||
override fun call(): Boolean {
|
||||
subFlow(IdentitySyncFlow.Send(otherSide, tx))
|
||||
// Wait for the counterparty to indicate they're done
|
||||
return receive<Boolean>(otherSide).unwrap { it }
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(IdentitySyncFlowTests.Initiator::class)
|
||||
class Receive(val otherSide: Party): FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
subFlow(IdentitySyncFlow.Receive(otherSide))
|
||||
// Notify the initiator that we've finished syncing
|
||||
send(otherSide, true)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.GBP
|
||||
import net.corda.core.contracts.Issued
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.finance.GBP
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockServices
|
||||
import org.junit.After
|
||||
|
@ -0,0 +1,26 @@
|
||||
package net.corda.core.identity
|
||||
|
||||
import net.corda.core.crypto.entropyToKeyPair
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.testing.getTestPartyAndCertificate
|
||||
import net.corda.testing.withTestSerialization
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.junit.Test
|
||||
import java.math.BigInteger
|
||||
|
||||
class PartyAndCertificateTest {
|
||||
@Test
|
||||
fun `kryo serialisation`() {
|
||||
withTestSerialization {
|
||||
val original = getTestPartyAndCertificate(Party(
|
||||
X500Name("CN=Test Corp,O=Test Corp,L=Madrid,C=ES"),
|
||||
entropyToKeyPair(BigInteger.valueOf(83)).public))
|
||||
val copy = original.serialize().deserialize()
|
||||
assertThat(copy).isEqualTo(original).isNotSameAs(original)
|
||||
assertThat(copy.certPath).isEqualTo(original.certPath)
|
||||
assertThat(copy.certificate).isEqualTo(original.certificate)
|
||||
}
|
||||
}
|
||||
}
|
@ -17,8 +17,6 @@ class VaultUpdateTests {
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
}
|
||||
|
||||
override val legalContractReference: SecureHash = SecureHash.sha256("")
|
||||
}
|
||||
|
||||
private class DummyState : ContractState {
|
||||
|
@ -18,7 +18,7 @@ import net.corda.node.internal.InitiatedFlowFactory
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.node.services.persistence.schemas.requery.AttachmentEntity
|
||||
import net.corda.node.utilities.DatabaseTransactionManager
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
@ -53,13 +53,11 @@ private fun MockNetwork.MockNode.hackAttachment(attachmentId: SecureHash, conten
|
||||
* @see NodeAttachmentService.importAttachment
|
||||
*/
|
||||
private fun NodeAttachmentService.updateAttachment(attachmentId: SecureHash, data: ByteArray) {
|
||||
with(session) {
|
||||
withTransaction {
|
||||
update(AttachmentEntity().apply {
|
||||
attId = attachmentId
|
||||
content = data
|
||||
})
|
||||
}
|
||||
val session = DatabaseTransactionManager.current().session
|
||||
val attachment = session.get<NodeAttachmentService.DBAttachment>(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString())
|
||||
attachment?.let {
|
||||
attachment.content = data
|
||||
session.save(attachment)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,18 @@
|
||||
package net.corda.core.serialization
|
||||
|
||||
import net.corda.finance.contracts.CommercialPaper
|
||||
import net.corda.finance.contracts.asset.Cash
|
||||
import net.corda.testing.TestDependencyInjectionBase
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class CommandsSerializationTests : TestDependencyInjectionBase() {
|
||||
|
||||
@Test
|
||||
fun `test cash move serialization`() {
|
||||
val command = Cash.Commands.Move(CommercialPaper::class.java)
|
||||
val copiedCommand = command.serialize().deserialize()
|
||||
|
||||
assertEquals(command, copiedCommand)
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
package net.corda.core.serialization
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.finance.POUNDS
|
||||
import net.corda.testing.*
|
||||
import net.corda.testing.node.MockServices
|
||||
import org.junit.Before
|
||||
@ -19,8 +19,6 @@ val TEST_PROGRAM_ID = TransactionSerializationTests.TestCash()
|
||||
|
||||
class TransactionSerializationTests : TestDependencyInjectionBase() {
|
||||
class TestCash : Contract {
|
||||
override val legalContractReference = SecureHash.sha256("TestCash")
|
||||
|
||||
override fun verify(tx: LedgerTransaction) {
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user