Merge branch 'master' into sofus-generic-contract

This commit is contained in:
sofusmortensen 2016-06-08 00:23:45 +02:00
commit 134aae8a44
63 changed files with 1276 additions and 676 deletions

View File

@ -168,7 +168,7 @@ public class JavaCommercialPaper implements Contract {
if (!inputs.isEmpty()) { if (!inputs.isEmpty()) {
throw new IllegalStateException("Failed Requirement: there is no input state"); throw new IllegalStateException("Failed Requirement: there is no input state");
} }
if (output.faceValue.getPennies() == 0) { if (output.faceValue.getQuantity() == 0) {
throw new IllegalStateException("Failed Requirement: the face value is not zero"); throw new IllegalStateException("Failed Requirement: the face value is not zero");
} }

View File

@ -117,7 +117,7 @@ class CommercialPaper : Contract {
// Don't allow people to issue commercial paper under other entities identities. // Don't allow people to issue commercial paper under other entities identities.
"the issuance is signed by the claimed issuer of the paper" by "the issuance is signed by the claimed issuer of the paper" by
(output.issuance.party.owningKey in command.signers) (output.issuance.party.owningKey in command.signers)
"the face value is not zero" by (output.faceValue.pennies > 0) "the face value is not zero" by (output.faceValue.quantity > 0)
"the maturity date is not in the past" by (time < output.maturityDate) "the maturity date is not in the past" by (time < output.maturityDate)
// Don't allow an existing CP state to be replaced by this issuance. // Don't allow an existing CP state to be replaced by this issuance.
// TODO: Consider how to handle the case of mistaken issuances, or other need to patch. // TODO: Consider how to handle the case of mistaken issuances, or other need to patch.

View File

@ -88,7 +88,7 @@ class CrowdFund : Contract {
"there is no input state" by tx.inStates.filterIsInstance<State>().isEmpty() "there is no input state" by tx.inStates.filterIsInstance<State>().isEmpty()
"the transaction is signed by the owner of the crowdsourcing" by (command.signers.contains(outputCrowdFund.campaign.owner)) "the transaction is signed by the owner of the crowdsourcing" by (command.signers.contains(outputCrowdFund.campaign.owner))
"the output registration is empty of pledges" by (outputCrowdFund.pledges.isEmpty()) "the output registration is empty of pledges" by (outputCrowdFund.pledges.isEmpty())
"the output registration has a non-zero target" by (outputCrowdFund.campaign.target.pennies > 0) "the output registration has a non-zero target" by (outputCrowdFund.campaign.target.quantity > 0)
"the output registration has a name" by (outputCrowdFund.campaign.name.isNotBlank()) "the output registration has a name" by (outputCrowdFund.campaign.name.isNotBlank())
"the output registration has a closing time in the future" by (time < outputCrowdFund.campaign.closingTime) "the output registration has a closing time in the future" by (time < outputCrowdFund.campaign.closingTime)
"the output registration has an open state" by (!outputCrowdFund.closed) "the output registration has an open state" by (!outputCrowdFund.closed)

View File

@ -112,7 +112,7 @@ class FixedRatePaymentEvent(date: LocalDate,
} }
override val flow: Amount<Currency> get() = override val flow: Amount<Currency> get() =
Amount<Currency>(dayCountFactor.times(BigDecimal(notional.pennies)).times(rate.ratioUnit!!.value).toLong(), notional.token) Amount<Currency>(dayCountFactor.times(BigDecimal(notional.quantity)).times(rate.ratioUnit!!.value).toLong(), notional.token)
override fun toString(): String = override fun toString(): String =
"FixedRatePaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate : $flow" "FixedRatePaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate : $flow"
@ -138,7 +138,7 @@ class FloatingRatePaymentEvent(date: LocalDate,
override val flow: Amount<Currency> get() { override val flow: Amount<Currency> get() {
// TODO: Should an uncalculated amount return a zero ? null ? etc. // TODO: Should an uncalculated amount return a zero ? null ? etc.
val v = rate.ratioUnit?.value ?: return Amount<Currency>(0, notional.token) val v = rate.ratioUnit?.value ?: return Amount<Currency>(0, notional.token)
return Amount<Currency>(dayCountFactor.times(BigDecimal(notional.pennies)).times(v).toLong(), notional.token) return Amount<Currency>(dayCountFactor.times(BigDecimal(notional.quantity)).times(v).toLong(), notional.token)
} }
override fun toString(): String = "FloatingPaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate (fix on $fixingDate): $flow" override fun toString(): String = "FloatingPaymentEvent $accrualStartDate -> $accrualEndDate : $dayCountFactor : $days : $date : $notional : $rate (fix on $fixingDate): $flow"
@ -456,7 +456,7 @@ class InterestRateSwap() : Contract {
fun checkLegAmounts(legs: Array<CommonLeg>) { fun checkLegAmounts(legs: Array<CommonLeg>) {
requireThat { requireThat {
"The notional is non zero" by legs.any { it.notional.pennies > (0).toLong() } "The notional is non zero" by legs.any { it.notional.quantity > (0).toLong() }
"The notional for all legs must be the same" by legs.all { it.notional == legs[0].notional } "The notional for all legs must be the same" by legs.all { it.notional == legs[0].notional }
} }
for (leg: CommonLeg in legs) { for (leg: CommonLeg in legs) {
@ -505,7 +505,7 @@ class InterestRateSwap() : Contract {
"There are no in states for an agreement" by inputs.isEmpty() "There are no in states for an agreement" by inputs.isEmpty()
"There are events in the fix schedule" by (irs.calculation.fixedLegPaymentSchedule.size > 0) "There are events in the fix schedule" by (irs.calculation.fixedLegPaymentSchedule.size > 0)
"There are events in the float schedule" by (irs.calculation.floatingLegPaymentSchedule.size > 0) "There are events in the float schedule" by (irs.calculation.floatingLegPaymentSchedule.size > 0)
"All notionals must be non zero" by (irs.fixedLeg.notional.pennies > 0 && irs.floatingLeg.notional.pennies > 0) "All notionals must be non zero" by (irs.fixedLeg.notional.quantity > 0 && irs.floatingLeg.notional.quantity > 0)
"The fixed leg rate must be positive" by (irs.fixedLeg.fixedRate.isPositive()) "The fixed leg rate must be positive" by (irs.fixedLeg.fixedRate.isPositive())
"The currency of the notionals must be the same" by (irs.fixedLeg.notional.token == irs.floatingLeg.notional.token) "The currency of the notionals must be the same" by (irs.fixedLeg.notional.token == irs.floatingLeg.notional.token)
"All leg notionals must be the same" by (irs.fixedLeg.notional == irs.floatingLeg.notional) "All leg notionals must be the same" by (irs.fixedLeg.notional == irs.floatingLeg.notional)

View File

@ -95,7 +95,7 @@ class ReferenceRate(val oracle: String, val tenor: Tenor, val name: String) : Fl
} }
// TODO: For further discussion. // TODO: For further discussion.
operator fun Amount<Currency>.times(other: RatioUnit): Amount<Currency> = Amount<Currency>((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.token) operator fun Amount<Currency>.times(other: RatioUnit): Amount<Currency> = Amount<Currency>((BigDecimal(this.quantity).multiply(other.value)).longValueExact(), this.token)
//operator fun Amount<Currency>.times(other: FixedRate): Amount<Currency> = Amount<Currency>((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.currency) //operator fun Amount<Currency>.times(other: FixedRate): Amount<Currency> = Amount<Currency>((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.currency)
//fun Amount<Currency>.times(other: InterestRateSwap.RatioUnit): Amount<Currency> = Amount<Currency>((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.currency) //fun Amount<Currency>.times(other: InterestRateSwap.RatioUnit): Amount<Currency> = Amount<Currency>((BigDecimal(this.pennies).multiply(other.value)).longValueExact(), this.currency)

View File

@ -8,8 +8,8 @@ import java.util.*
* Subset of cash-like contract state, containing the issuance definition. If these definitions match for two * Subset of cash-like contract state, containing the issuance definition. If these definitions match for two
* contracts' states, those states can be aggregated. * contracts' states, those states can be aggregated.
*/ */
interface CashIssuanceDefinition : IssuanceDefinition { interface AssetIssuanceDefinition<T> : IssuanceDefinition {
/** Where the underlying currency backing this ledger entry can be found (propagated) */ /** Where the underlying asset backing this ledger entry can be found (propagated) */
val deposit: PartyAndReference val deposit: PartyAndReference
val currency: Currency val token: T
} }

View File

@ -18,8 +18,6 @@ import java.util.*
val CASH_PROGRAM_ID = Cash() val CASH_PROGRAM_ID = Cash()
//SecureHash.sha256("cash") //SecureHash.sha256("cash")
class InsufficientBalanceException(val amountMissing: Amount<Currency>) : Exception()
/** /**
* A cash transaction may split and merge money represented by a set of (issuer, depositRef) pairs, across multiple * A cash transaction may split and merge money represented by a set of (issuer, depositRef) pairs, across multiple
* input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour * input and output states. Imagine a Bitcoin transaction but in which all UTXOs had a colour
@ -33,7 +31,7 @@ class InsufficientBalanceException(val amountMissing: Amount<Currency>) : Except
* At the same time, other contracts that just want money and don't care much who is currently holding it in their * At the same time, other contracts that just want money and don't care much who is currently holding it in their
* vaults can ignore the issuer/depositRefs and just examine the amount fields. * vaults can ignore the issuer/depositRefs and just examine the amount fields.
*/ */
class Cash : Contract { class Cash : FungibleAsset<Currency>() {
/** /**
* TODO: * TODO:
* 1) hash should be of the contents, not the URI * 1) hash should be of the contents, not the URI
@ -46,12 +44,12 @@ class Cash : Contract {
*/ */
override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html") override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html")
data class IssuanceDefinition( data class IssuanceDefinition<T>(
/** Where the underlying currency backing this ledger entry can be found (propagated) */ /** Where the underlying currency backing this ledger entry can be found (propagated) */
override val deposit: PartyAndReference, override val deposit: PartyAndReference,
override val currency: Currency override val token: T
) : CashIssuanceDefinition ) : AssetIssuanceDefinition<T>
/** A state representing a cash claim against some party */ /** A state representing a cash claim against some party */
data class State( data class State(
@ -64,9 +62,9 @@ class Cash : Contract {
override val owner: PublicKey, override val owner: PublicKey,
override val notary: Party override val notary: Party
) : CommonCashState<Cash.IssuanceDefinition> { ) : FungibleAsset.State<Currency> {
override val issuanceDef: Cash.IssuanceDefinition override val issuanceDef: IssuanceDefinition<Currency>
get() = Cash.IssuanceDefinition(deposit, amount.token) get() = IssuanceDefinition(deposit, amount.token)
override val contract = CASH_PROGRAM_ID override val contract = CASH_PROGRAM_ID
override fun toString() = "${Emoji.bagOfCash}Cash($amount at $deposit owned by ${owner.toStringShort()})" override fun toString() = "${Emoji.bagOfCash}Cash($amount at $deposit owned by ${owner.toStringShort()})"
@ -76,93 +74,26 @@ class Cash : Contract {
// Just for grouping // Just for grouping
interface Commands : CommandData { interface Commands : CommandData {
class Move() : TypeOnlyCommandData(), Commands class Move() : TypeOnlyCommandData(), FungibleAsset.Commands.Move
/** /**
* Allows new cash states to be issued into existence: the nonce ("number used once") ensures the transaction * Allows new cash states to be issued into existence: the nonce ("number used once") ensures the transaction
* has a unique ID even when there are no inputs. * has a unique ID even when there are no inputs.
*/ */
data class Issue(val nonce: Long = SecureRandom.getInstanceStrong().nextLong()) : Commands data class Issue(override val nonce: Long = SecureRandom.getInstanceStrong().nextLong()) : FungibleAsset.Commands.Issue
/** /**
* A command stating that money has been withdrawn from the shared ledger and is now accounted for * A command stating that money has been withdrawn from the shared ledger and is now accounted for
* in some other way. * in some other way.
*/ */
data class Exit(val amount: Amount<Currency>) : Commands data class Exit(override val amount: Amount<Currency>) : Commands, FungibleAsset.Commands.Exit<Currency>
}
/** This is the function EVERYONE runs */
override fun verify(tx: TransactionForVerification) {
// Each group is a set of input/output states with distinct (deposit, currency) attributes. These types
// of cash are not fungible and must be kept separated for bookkeeping purposes.
val groups = tx.groupStates() { it: Cash.State -> it.issuanceDef }
for ((inputs, outputs, key) in groups) {
// Either inputs or outputs could be empty.
val deposit = key.deposit
val currency = key.currency
val issuer = deposit.party
requireThat {
"there are no zero sized outputs" by outputs.none { it.amount.pennies == 0L }
}
val issueCommand = tx.commands.select<Commands.Issue>().firstOrNull()
if (issueCommand != null) {
verifyIssueCommand(inputs, outputs, tx, issueCommand, currency, issuer)
} else {
val inputAmount = inputs.sumCashOrNull() ?: throw IllegalArgumentException("there is at least one cash input for this group")
val outputAmount = outputs.sumCashOrZero(currency)
// If we want to remove cash from the ledger, that must be signed for by the issuer.
// A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero.
val exitCommand = tx.commands.select<Commands.Exit>(party = issuer).singleOrNull()
val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, currency)
requireThat {
"there are no zero sized inputs" by inputs.none { it.amount.pennies == 0L }
"for deposit ${deposit.reference} at issuer ${deposit.party.name} the amounts balance" by
(inputAmount == outputAmount + amountExitingLedger)
}
verifyMoveCommands<Commands.Move>(inputs, tx)
}
}
}
private fun verifyIssueCommand(inputs: List<State>,
outputs: List<State>,
tx: TransactionForVerification,
issueCommand: AuthenticatedObject<Commands.Issue>,
currency: Currency,
issuer: Party) {
// If we have an issue command, perform special processing: the group is allowed to have no inputs,
// and the output states must have a deposit reference owned by the signer.
//
// Whilst the transaction *may* have no inputs, it can have them, and in this case the outputs must
// sum to more than the inputs. An issuance of zero size is not allowed.
//
// Note that this means literally anyone with access to the network can issue cash claims of arbitrary
// amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some
// as-yet-unwritten identity service. See ADP-22 for discussion.
// The grouping ensures that all outputs have the same deposit reference and currency.
val inputAmount = inputs.sumCashOrZero(currency)
val outputAmount = outputs.sumCash()
val cashCommands = tx.commands.select<Cash.Commands>()
requireThat {
"the issue command has a nonce" by (issueCommand.value.nonce != 0L)
"output deposits are owned by a command signer" by (issuer in issueCommand.signingParties)
"output values sum to more than the inputs" by (outputAmount > inputAmount)
"there is only a single issue command" by (cashCommands.count() == 1)
}
} }
/** /**
* Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey. * Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey.
*/ */
fun generateIssue(tx: TransactionBuilder, issuanceDef: CashIssuanceDefinition, pennies: Long, owner: PublicKey, notary: Party) fun generateIssue(tx: TransactionBuilder, issuanceDef: AssetIssuanceDefinition<Currency>, pennies: Long, owner: PublicKey, notary: Party)
= generateIssue(tx, Amount(pennies, issuanceDef.currency), issuanceDef.deposit, owner, notary) = generateIssue(tx, Amount(pennies, issuanceDef.token), issuanceDef.deposit, owner, notary)
/** /**
* Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey.
@ -234,7 +165,7 @@ class Cash : Contract {
State(deposit, totalAmount, to, coins.first().state.notary) State(deposit, totalAmount, to, coins.first().state.notary)
} }
val outputs = if (change.pennies > 0) { val outputs = if (change.quantity > 0) {
// Just copy a key across as the change key. In real life of course, this works but leaks private data. // Just copy a key across as the change key. In real life of course, this works but leaks private data.
// In bitcoinj we derive a fresh key here and then shuffle the outputs to ensure it's hard to follow // In bitcoinj we derive a fresh key here and then shuffle the outputs to ensure it's hard to follow
// value flows through the transaction graph. // value flows through the transaction graph.

View File

@ -0,0 +1,148 @@
package com.r3corda.contracts.cash
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.toStringShort
import com.r3corda.core.utilities.Emoji
import java.security.PublicKey
import java.security.SecureRandom
import java.util.*
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Cash-like
//
class InsufficientBalanceException(val amountMissing: Amount<*>) : Exception()
/**
* Superclass for contracts representing assets which are fungible, countable and issued by a specific party. States
* contain assets which are equivalent (such as cash of the same currency), so records of their existence can
* be merged or split as needed where the issuer is the same. For instance, dollars issued by the Fed are fungible and
* countable (in cents), barrels of West Texas 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 subclass that implements currency.
*
* @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.)
*/
abstract class FungibleAsset<T> : Contract {
/** A state representing a claim against some party */
interface State<T> : FungibleAssetState<T, AssetIssuanceDefinition<T>> {
/** Where the underlying asset backing this ledger entry can be found (propagated) */
override val deposit: PartyAndReference
override val amount: Amount<T>
/** There must be a MoveCommand signed by this key to claim the amount */
override val owner: PublicKey
override val notary: Party
}
// Just for grouping
interface Commands : CommandData {
interface Move : 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 : Commands { val nonce: Long }
/**
* A command stating that money has been withdrawn from the shared ledger and is now accounted for
* in some other way.
*/
interface Exit<T> : Commands { val amount: Amount<T> }
}
/** This is the function EVERYONE runs */
override fun verify(tx: TransactionForVerification) {
// Each group is a set of input/output states with distinct issuance definitions. These assets are not fungible
// and must be kept separated for bookkeeping purposes.
val groups = tx.groupStates() { it: FungibleAsset.State<T> -> it.issuanceDef }
for ((inputs, outputs, key) in groups) {
// Either inputs or outputs could be empty.
val deposit = key.deposit
val token = key.token
val issuer = deposit.party
requireThat {
"there are no zero sized outputs" by outputs.none { it.amount.quantity == 0L }
}
val issueCommand = tx.commands.select<Commands.Issue>().firstOrNull()
if (issueCommand != null) {
verifyIssueCommand(inputs, outputs, tx, issueCommand, token, issuer)
} else {
val inputAmount = inputs.sumFungibleOrNull<T>() ?: throw IllegalArgumentException("there is at least one asset input for this group")
val outputAmount = outputs.sumFungibleOrZero(token)
// If we want to remove assets from the ledger, that must be signed for by the issuer.
// A mis-signed or duplicated exit command will just be ignored here and result in the exit amount being zero.
val exitCommand = tx.commands.select<Commands.Exit<T>>(party = issuer).singleOrNull()
val amountExitingLedger = exitCommand?.value?.amount ?: Amount(0, token)
requireThat {
"there are no zero sized inputs" by inputs.none { it.amount.quantity == 0L }
"for deposit ${deposit.reference} at issuer ${deposit.party.name} the amounts balance" by
(inputAmount == outputAmount + amountExitingLedger)
}
verifyMoveCommands<Commands.Move>(inputs, tx)
}
}
}
private fun verifyIssueCommand(inputs: List<State<T>>,
outputs: List<State<T>>,
tx: TransactionForVerification,
issueCommand: AuthenticatedObject<Commands.Issue>,
token: T,
issuer: Party) {
// If we have an issue command, perform special processing: the group is allowed to have no inputs,
// and the output states must have a deposit reference owned by the signer.
//
// Whilst the transaction *may* have no inputs, it can have them, and in this case the outputs must
// sum to more than the inputs. An issuance of zero size is not allowed.
//
// Note that this means literally anyone with access to the network can issue asset claims of arbitrary
// amounts! It is up to the recipient to decide if the backing party is trustworthy or not, via some
// external mechanism (such as locally defined rules on which parties are trustworthy).
// The grouping ensures that all outputs have the same deposit reference and token.
val inputAmount = inputs.sumFungibleOrZero(token)
val outputAmount = outputs.sumFungible<T>()
val assetCommands = tx.commands.select<FungibleAsset.Commands>()
requireThat {
"the issue command has a nonce" by (issueCommand.value.nonce != 0L)
"output deposits are owned by a command signer" by (issuer in issueCommand.signingParties)
"output values sum to more than the inputs" by (outputAmount > inputAmount)
"there is only a single issue command" by (assetCommands.count() == 1)
}
}
}
// Small DSL extensions.
/**
* Sums the asset states in the list belonging to a single owner, throwing an exception
* if there are none, or if any of the asset states cannot be added together (i.e. are
* different tokens).
*/
fun <T> Iterable<ContractState>.sumFungibleBy(owner: PublicKey) = filterIsInstance<FungibleAsset.State<T>>().filter { it.owner == owner }.map { it.amount }.sumOrThrow()
/**
* Sums the asset states in the list, throwing an exception if there are none, or if any of the asset
* states cannot be added together (i.e. are different tokens).
*/
fun <T> Iterable<ContractState>.sumFungible() = filterIsInstance<FungibleAsset.State<T>>().map { it.amount }.sumOrThrow()
/** Sums the asset states in the list, returning null if there are none. */
fun <T> Iterable<ContractState>.sumFungibleOrNull() = filterIsInstance<FungibleAsset.State<T>>().map { it.amount }.sumOrNull()
/** Sums the asset states in the list, returning zero of the given token if there are none. */
fun <T> Iterable<ContractState>.sumFungibleOrZero(token: T) = filterIsInstance<FungibleAsset.State<T>>().map { it.amount }.sumOrZero(token)

View File

@ -8,9 +8,9 @@ import java.util.Currency
/** /**
* Common elements of cash contract states. * Common elements of cash contract states.
*/ */
interface CommonCashState<I : CashIssuanceDefinition> : OwnableState { interface FungibleAssetState<T, I : AssetIssuanceDefinition<T>> : OwnableState {
val issuanceDef: I val issuanceDef: I
/** Where the underlying currency backing this ledger entry can be found (propagated) */ /** Where the underlying currency backing this ledger entry can be found (propagated) */
val deposit: PartyAndReference val deposit: PartyAndReference
val amount: Amount<Currency> val amount: Amount<T>
} }

View File

@ -19,7 +19,7 @@ data class Zero(val dummy: Int = 0) : Kontract
// should be replaced with something that uses Corda assets and/or cash // should be replaced with something that uses Corda assets and/or cash
data class Transfer(val amount: Observable<Long>, val currency: Currency, val from: Party, val to: Party) : Kontract { data class Transfer(val amount: Observable<Long>, val currency: Currency, val from: Party, val to: Party) : Kontract {
constructor(amount: Amount<Currency>, from: Party, to: Party ) : this(const(amount.pennies), amount.token, from, to) constructor(amount: Amount<Currency>, from: Party, to: Party ) : this(const(amount.quantity), amount.token, from, to)
} }
data class And(val kontracts: Set<Kontract>) : Kontract data class And(val kontracts: Set<Kontract>) : Kontract

View File

@ -97,7 +97,7 @@ object TwoPartyTradeProtocol {
@Suspendable @Suspendable
private fun getNotarySignature(stx: SignedTransaction): DigitalSignature.LegallyIdentifiable { private fun getNotarySignature(stx: SignedTransaction): DigitalSignature.LegallyIdentifiable {
progressTracker.currentStep = NOTARY progressTracker.currentStep = NOTARY
return subProtocol(NotaryProtocol(stx.tx)) return subProtocol(NotaryProtocol.Client(stx.tx))
} }
@Suspendable @Suspendable

View File

@ -340,18 +340,18 @@ class IRSTests {
fun `expression calculation testing`() { fun `expression calculation testing`() {
val dummyIRS = singleIRS() val dummyIRS = singleIRS()
val stuffToPrint: ArrayList<String> = arrayListOf( val stuffToPrint: ArrayList<String> = arrayListOf(
"fixedLeg.notional.pennies", "fixedLeg.notional.quantity",
"fixedLeg.fixedRate.ratioUnit", "fixedLeg.fixedRate.ratioUnit",
"fixedLeg.fixedRate.ratioUnit.value", "fixedLeg.fixedRate.ratioUnit.value",
"floatingLeg.notional.pennies", "floatingLeg.notional.quantity",
"fixedLeg.fixedRate", "fixedLeg.fixedRate",
"currentBusinessDate", "currentBusinessDate",
"calculation.floatingLegPaymentSchedule.get(currentBusinessDate)", "calculation.floatingLegPaymentSchedule.get(currentBusinessDate)",
"fixedLeg.notional.token.currencyCode", "fixedLeg.notional.token.currencyCode",
"fixedLeg.notional.pennies * 10", "fixedLeg.notional.quantity * 10",
"fixedLeg.notional.pennies * fixedLeg.fixedRate.ratioUnit.value", "fixedLeg.notional.quantity * fixedLeg.fixedRate.ratioUnit.value",
"(fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360 ", "(fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360 ",
"(fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value))" "(fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value))"
// "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate" // "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate"
// "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value", // "calculation.floatingLegPaymentSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value",
//"( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) - (floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))", //"( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) - (floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
@ -450,7 +450,7 @@ class IRSTests {
val irs = singleIRS() val irs = singleIRS()
transaction { transaction {
output() { output() {
irs.copy(irs.fixedLeg.copy(notional = irs.fixedLeg.notional.copy(pennies = 0))) irs.copy(irs.fixedLeg.copy(notional = irs.fixedLeg.notional.copy(quantity = 0)))
} }
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME) timestamp(TEST_TX_TIME)
@ -459,7 +459,7 @@ class IRSTests {
transaction { transaction {
output() { output() {
irs.copy(irs.fixedLeg.copy(notional = irs.floatingLeg.notional.copy(pennies = 0))) irs.copy(irs.fixedLeg.copy(notional = irs.floatingLeg.notional.copy(quantity = 0)))
} }
arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } arg(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() }
timestamp(TEST_TX_TIME) timestamp(TEST_TX_TIME)
@ -487,7 +487,7 @@ class IRSTests {
@Test @Test
fun `ensure same currency notionals`() { fun `ensure same currency notionals`() {
val irs = singleIRS() val irs = singleIRS()
val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.fixedLeg.notional.pennies, Currency.getInstance("JPY")))) val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.fixedLeg.notional.quantity, Currency.getInstance("JPY"))))
transaction { transaction {
output() { output() {
modifiedIRS modifiedIRS
@ -501,7 +501,7 @@ class IRSTests {
@Test @Test
fun `ensure notional amounts are equal`() { fun `ensure notional amounts are equal`() {
val irs = singleIRS() val irs = singleIRS()
val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.floatingLeg.notional.pennies + 1, irs.floatingLeg.notional.token))) val modifiedIRS = irs.copy(fixedLeg = irs.fixedLeg.copy(notional = Amount(irs.floatingLeg.notional.quantity + 1, irs.floatingLeg.notional.token)))
transaction { transaction {
output() { output() {
modifiedIRS modifiedIRS
@ -619,7 +619,7 @@ class IRSTests {
val firstResetKey = newIRS.calculation.floatingLegPaymentSchedule.keys.first() val firstResetKey = newIRS.calculation.floatingLegPaymentSchedule.keys.first()
val firstResetValue = newIRS.calculation.floatingLegPaymentSchedule[firstResetKey] val firstResetValue = newIRS.calculation.floatingLegPaymentSchedule[firstResetKey]
var modifiedFirstResetValue = firstResetValue!!.copy(notional = Amount(firstResetValue.notional.pennies, Currency.getInstance("JPY"))) var modifiedFirstResetValue = firstResetValue!!.copy(notional = Amount(firstResetValue.notional.quantity, Currency.getInstance("JPY")))
output() { output() {
newIRS.copy( newIRS.copy(
@ -640,7 +640,7 @@ class IRSTests {
arg(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) } arg(ORACLE_PUBKEY) { Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd) }
val latestReset = newIRS.calculation.floatingLegPaymentSchedule.filter { it.value.rate is FixedRate }.maxBy { it.key } val latestReset = newIRS.calculation.floatingLegPaymentSchedule.filter { it.value.rate is FixedRate }.maxBy { it.key }
var modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.pennies, Currency.getInstance("JPY"))) var modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.quantity, Currency.getInstance("JPY")))
output() { output() {
newIRS.copy( newIRS.copy(

View File

@ -41,7 +41,7 @@ class CashTests {
tweak { tweak {
output { outState } output { outState }
// No command arguments // No command arguments
this `fails requirement` "required com.r3corda.contracts.cash.Cash.Commands.Move command" this `fails requirement` "required com.r3corda.contracts.cash.FungibleAsset.Commands.Move command"
} }
tweak { tweak {
output { outState } output { outState }
@ -52,7 +52,7 @@ class CashTests {
output { outState } output { outState }
output { outState `issued by` MINI_CORP } output { outState `issued by` MINI_CORP }
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
this `fails requirement` "at least one cash input" this `fails requirement` "at least one asset input"
} }
// Simple reallocation works. // Simple reallocation works.
tweak { tweak {
@ -71,7 +71,7 @@ class CashTests {
output { outState } output { outState }
arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() } arg(MINI_CORP_PUBKEY) { Cash.Commands.Move() }
this `fails requirement` "there is at least one cash input" this `fails requirement` "there is at least one asset input"
} }
// Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised // Check we can issue money only as long as the issuer institution is a command signer, i.e. any recognised
@ -112,7 +112,7 @@ class CashTests {
// Test issuance from the issuance definition // Test issuance from the issuance definition
val issuanceDef = Cash.IssuanceDefinition(MINI_CORP.ref(12, 34), USD) val issuanceDef = Cash.IssuanceDefinition(MINI_CORP.ref(12, 34), USD)
val templatePtx = TransactionBuilder() val templatePtx = TransactionBuilder()
Cash().generateIssue(templatePtx, issuanceDef, 100.DOLLARS.pennies, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) Cash().generateIssue(templatePtx, issuanceDef, 100.DOLLARS.quantity, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY)
assertTrue(templatePtx.inputStates().isEmpty()) assertTrue(templatePtx.inputStates().isEmpty())
assertEquals(ptx.outputStates()[0], templatePtx.outputStates()[0]) assertEquals(ptx.outputStates()[0], templatePtx.outputStates()[0])
@ -297,7 +297,7 @@ class CashTests {
tweak { tweak {
arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) } arg(MEGA_CORP_PUBKEY) { Cash.Commands.Exit(200.DOLLARS) }
this `fails requirement` "required com.r3corda.contracts.cash.Cash.Commands.Move command" this `fails requirement` "required com.r3corda.contracts.cash.FungibleAsset.Commands.Move command"
tweak { tweak {
arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() } arg(DUMMY_PUBKEY_1) { Cash.Commands.Move() }

View File

@ -30,6 +30,8 @@ val Double.bd: BigDecimal get() = BigDecimal(this)
val String.bd: BigDecimal get() = BigDecimal(this) val String.bd: BigDecimal get() = BigDecimal(this)
val Long.bd: BigDecimal get() = BigDecimal(this) val Long.bd: BigDecimal get() = BigDecimal(this)
fun String.abbreviate(maxWidth: Int): String = if (length <= maxWidth) this else take(maxWidth - 1) + ""
/** /**
* Returns a random positive long generated using a secure RNG. This function sacrifies a bit of entropy in order to * Returns a random positive long generated using a secure RNG. This function sacrifies a bit of entropy in order to
* avoid potential bugs where the value is used in a context where negative numbers are not expected. * avoid potential bugs where the value is used in a context where negative numbers are not expected.

View File

@ -33,40 +33,40 @@ import java.util.*
* *
* @param T the type of the token, for example [Currency]. * @param T the type of the token, for example [Currency].
*/ */
data class Amount<T>(val pennies: Long, val token: T) : Comparable<Amount<T>> { data class Amount<T>(val quantity: Long, val token: T) : Comparable<Amount<T>> {
init { init {
// Negative amounts are of course a vital part of any ledger, but negative values are only valid in certain // Negative amounts are of course a vital part of any ledger, but negative values are only valid in certain
// contexts: you cannot send a negative amount of cash, but you can (sometimes) have a negative balance. // contexts: you cannot send a negative amount of cash, but you can (sometimes) have a negative balance.
// If you want to express a negative amount, for now, use a long. // If you want to express a negative amount, for now, use a long.
require(pennies >= 0) { "Negative amounts are not allowed: $pennies" } require(quantity >= 0) { "Negative amounts are not allowed: $quantity" }
} }
constructor(amount: BigDecimal, currency: T) : this(amount.toLong(), currency) constructor(amount: BigDecimal, currency: T) : this(amount.toLong(), currency)
operator fun plus(other: Amount<T>): Amount<T> { operator fun plus(other: Amount<T>): Amount<T> {
checkCurrency(other) checkCurrency(other)
return Amount(Math.addExact(pennies, other.pennies), token) return Amount(Math.addExact(quantity, other.quantity), token)
} }
operator fun minus(other: Amount<T>): Amount<T> { operator fun minus(other: Amount<T>): Amount<T> {
checkCurrency(other) checkCurrency(other)
return Amount(Math.subtractExact(pennies, other.pennies), token) return Amount(Math.subtractExact(quantity, other.quantity), token)
} }
private fun checkCurrency(other: Amount<T>) { private fun checkCurrency(other: Amount<T>) {
require(other.token == token) { "Currency mismatch: ${other.token} vs $token" } require(other.token == token) { "Currency mismatch: ${other.token} vs $token" }
} }
operator fun div(other: Long): Amount<T> = Amount(pennies / other, token) operator fun div(other: Long): Amount<T> = Amount(quantity / other, token)
operator fun times(other: Long): Amount<T> = Amount(Math.multiplyExact(pennies, other), token) operator fun times(other: Long): Amount<T> = Amount(Math.multiplyExact(quantity, other), token)
operator fun div(other: Int): Amount<T> = Amount(pennies / other, token) operator fun div(other: Int): Amount<T> = Amount(quantity / other, token)
operator fun times(other: Int): Amount<T> = Amount(Math.multiplyExact(pennies, other.toLong()), token) operator fun times(other: Int): Amount<T> = Amount(Math.multiplyExact(quantity, other.toLong()), token)
override fun toString(): String = (BigDecimal(pennies).divide(BigDecimal(100))).setScale(2).toPlainString() override fun toString(): String = (BigDecimal(quantity).divide(BigDecimal(100))).setScale(2).toPlainString()
override fun compareTo(other: Amount<T>): Int { override fun compareTo(other: Amount<T>): Int {
checkCurrency(other) checkCurrency(other)
return pennies.compareTo(other.pennies) return quantity.compareTo(other.quantity)
} }
} }

View File

@ -4,6 +4,7 @@ import com.google.common.util.concurrent.ListenableFuture
import com.r3corda.core.serialization.serialize import com.r3corda.core.serialization.serialize
import java.time.Instant import java.time.Instant
import java.util.concurrent.Executor import java.util.concurrent.Executor
import java.util.concurrent.atomic.AtomicBoolean
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
/** /**
@ -68,14 +69,17 @@ interface MessagingService {
* take the registration object, unlike the callback to [MessagingService.addMessageHandler]. * take the registration object, unlike the callback to [MessagingService.addMessageHandler].
*/ */
fun MessagingService.runOnNextMessage(topic: String = "", executor: Executor? = null, callback: (Message) -> Unit) { fun MessagingService.runOnNextMessage(topic: String = "", executor: Executor? = null, callback: (Message) -> Unit) {
val consumed = AtomicBoolean()
addMessageHandler(topic, executor) { msg, reg -> addMessageHandler(topic, executor) { msg, reg ->
removeMessageHandler(reg) removeMessageHandler(reg)
check(!consumed.getAndSet(true)) { "Called more than once" }
check(msg.topic == topic) { "Topic mismatch: ${msg.topic} vs $topic" }
callback(msg) callback(msg)
} }
} }
fun MessagingService.send(topic: String, obj: Any, to: MessageRecipients) { fun MessagingService.send(topic: String, payload: Any, to: MessageRecipients) {
send(createMessage(topic, obj.serialize().bits), to) send(createMessage(topic, payload.serialize().bits), to)
} }
/** /**

View File

@ -12,9 +12,8 @@ import java.time.Clock
* mocked out. This class is useful to pass to chunks of pluggable code that might have need of many different kinds of * mocked out. This class is useful to pass to chunks of pluggable code that might have need of many different kinds of
* functionality and you don't want to hard-code which types in the interface. * functionality and you don't want to hard-code which types in the interface.
* *
* All services exposed to protocols (public view) need to implement [SerializeAsToken] or similar to avoid being serialized in checkpoints. * Any services exposed to protocols (public view) need to implement [SerializeAsToken] or similar to avoid their internal
* * state from being serialized in checkpoints.
* TODO: Split into a public (to contracts etc) and private (to node) view
*/ */
interface ServiceHub { interface ServiceHub {
val walletService: WalletService val walletService: WalletService

View File

@ -1,11 +1,3 @@
/*
* Copyright 2016 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
* set forth therein.
*
* All other rights reserved.
*/
package com.r3corda.core.node.services package com.r3corda.core.node.services
/** /**
@ -29,6 +21,8 @@ abstract class ServiceType(val id: String) {
false false
} }
fun isSubTypeOf(superType: ServiceType) = (id == superType.id) || id.startsWith(superType.id + ".")
override fun hashCode(): Int = id.hashCode() override fun hashCode(): Int = id.hashCode()
override fun toString(): String = id.toString() override fun toString(): String = id.toString()
} }

View File

@ -1,4 +1,4 @@
package com.r3corda.node.services.transactions package com.r3corda.core.node.services
import com.r3corda.core.contracts.TimestampCommand import com.r3corda.core.contracts.TimestampCommand
import com.r3corda.core.seconds import com.r3corda.core.seconds
@ -9,7 +9,7 @@ import java.time.Duration
/** /**
* Checks if the given timestamp falls within the allowed tolerance interval * Checks if the given timestamp falls within the allowed tolerance interval
*/ */
class TimestampChecker(val clock: Clock = Clock.systemDefaultZone(), class TimestampChecker(val clock: Clock = Clock.systemUTC(),
val tolerance: Duration = 30.seconds) { val tolerance: Duration = 30.seconds) {
fun isValid(timestampCommand: TimestampCommand): Boolean { fun isValid(timestampCommand: TimestampCommand): Boolean {
val before = timestampCommand.before val before = timestampCommand.before

View File

@ -10,6 +10,7 @@ import com.r3corda.core.node.services.AttachmentStorage
import com.r3corda.core.node.services.IdentityService import com.r3corda.core.node.services.IdentityService
import com.r3corda.core.node.services.KeyManagementService import com.r3corda.core.node.services.KeyManagementService
import com.r3corda.core.node.services.StorageService import com.r3corda.core.node.services.StorageService
import com.r3corda.core.serialization.SingletonSerializeAsToken
import com.r3corda.core.utilities.RecordingMap import com.r3corda.core.utilities.RecordingMap
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
@ -24,7 +25,7 @@ import java.util.jar.JarInputStream
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
@ThreadSafe @ThreadSafe
class MockIdentityService(val identities: List<Party>) : IdentityService { class MockIdentityService(val identities: List<Party>) : IdentityService, SingletonSerializeAsToken() {
private val keyToParties: Map<PublicKey, Party> private val keyToParties: Map<PublicKey, Party>
get() = synchronized(identities) { identities.associateBy { it.owningKey } } get() = synchronized(identities) { identities.associateBy { it.owningKey } }
private val nameToParties: Map<String, Party> private val nameToParties: Map<String, Party>
@ -36,7 +37,7 @@ class MockIdentityService(val identities: List<Party>) : IdentityService {
} }
class MockKeyManagementService(vararg initialKeys: KeyPair) : KeyManagementService { class MockKeyManagementService(vararg initialKeys: KeyPair) : SingletonSerializeAsToken(), KeyManagementService {
override val keys: MutableMap<PublicKey, PrivateKey> override val keys: MutableMap<PublicKey, PrivateKey>
init { init {
@ -88,7 +89,7 @@ class MockStorageService(override val attachments: AttachmentStorage = MockAttac
override val myLegalIdentity: Party = Party("Unit test party", myLegalIdentityKey.public), override val myLegalIdentity: Party = Party("Unit test party", myLegalIdentityKey.public),
// This parameter is for unit tests that want to observe operation details. // This parameter is for unit tests that want to observe operation details.
val recordingAs: (String) -> String = { tableName -> "" }) val recordingAs: (String) -> String = { tableName -> "" })
: StorageService { : SingletonSerializeAsToken(), StorageService {
protected val tables = HashMap<String, MutableMap<*, *>>() protected val tables = HashMap<String, MutableMap<*, *>>()
private fun <K, V> getMapOriginal(tableName: String): MutableMap<K, V> { private fun <K, V> getMapOriginal(tableName: String): MutableMap<K, V> {

View File

@ -3,12 +3,14 @@ package com.r3corda.core.serialization
import co.paralleluniverse.fibers.Fiber import co.paralleluniverse.fibers.Fiber
import co.paralleluniverse.io.serialization.kryo.KryoSerializer import co.paralleluniverse.io.serialization.kryo.KryoSerializer
import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.Kryo.DefaultInstantiatorStrategy
import com.esotericsoftware.kryo.KryoException import com.esotericsoftware.kryo.KryoException
import com.esotericsoftware.kryo.Serializer import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.serializers.JavaSerializer import com.esotericsoftware.kryo.serializers.JavaSerializer
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.generateKeyPair import com.r3corda.core.crypto.generateKeyPair
import com.r3corda.core.crypto.sha256 import com.r3corda.core.crypto.sha256
@ -17,6 +19,8 @@ import com.r3corda.core.node.services.AttachmentStorage
import de.javakaffee.kryoserializers.ArraysAsListSerializer import de.javakaffee.kryoserializers.ArraysAsListSerializer
import org.objenesis.strategy.StdInstantiatorStrategy import org.objenesis.strategy.StdInstantiatorStrategy
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
@ -252,7 +256,7 @@ fun createKryo(k: Kryo = Kryo()): Kryo {
isRegistrationRequired = false isRegistrationRequired = false
// Allow construction of objects using a JVM backdoor that skips invoking the constructors, if there is no // Allow construction of objects using a JVM backdoor that skips invoking the constructors, if there is no
// no-arg constructor available. // no-arg constructor available.
instantiatorStrategy = Kryo.DefaultInstantiatorStrategy(StdInstantiatorStrategy()) instantiatorStrategy = DefaultInstantiatorStrategy(StdInstantiatorStrategy())
register(Arrays.asList("").javaClass, ArraysAsListSerializer()); register(Arrays.asList("").javaClass, ArraysAsListSerializer());
@ -262,18 +266,16 @@ fun createKryo(k: Kryo = Kryo()): Kryo {
register(Kryo::class.java, object : Serializer<Kryo>() { register(Kryo::class.java, object : Serializer<Kryo>() {
override fun write(kryo: Kryo, output: Output, obj: Kryo) { override fun write(kryo: Kryo, output: Output, obj: Kryo) {
} }
override fun read(kryo: Kryo, input: Input, type: Class<Kryo>): Kryo { override fun read(kryo: Kryo, input: Input, type: Class<Kryo>): Kryo {
return createKryo((Fiber.getFiberSerializer() as KryoSerializer).kryo) return createKryo((Fiber.getFiberSerializer() as KryoSerializer).kryo)
} }
}) })
// Some things where the JRE provides an efficient custom serialisation. // Some things where the JRE provides an efficient custom serialisation.
val ser = JavaSerializer()
val keyPair = generateKeyPair() val keyPair = generateKeyPair()
register(keyPair.public.javaClass, ser) register(keyPair.public.javaClass, ReferencesAwareJavaSerializer)
register(keyPair.private.javaClass, ser) register(keyPair.private.javaClass, ReferencesAwareJavaSerializer)
register(Instant::class.java, ser) register(Instant::class.java, ReferencesAwareJavaSerializer)
// Some classes have to be handled with the ImmutableClassSerializer because they need to have their // Some classes have to be handled with the ImmutableClassSerializer because they need to have their
// constructors be invoked (typically for lazy members). // constructors be invoked (typically for lazy members).
@ -284,6 +286,71 @@ fun createKryo(k: Kryo = Kryo()): Kryo {
// This ensures a SerializedBytes<Foo> wrapper is written out as just a byte array. // This ensures a SerializedBytes<Foo> wrapper is written out as just a byte array.
register(SerializedBytes::class.java, SerializedBytesSerializer) register(SerializedBytes::class.java, SerializedBytesSerializer)
addDefaultSerializer(SerializeAsToken::class.java, SerializeAsTokenSerializer<SerializeAsToken>())
// This is required to make all the unit tests pass
register(Party::class.java)
noReferencesWithin<WireTransaction>()
}
}
/**
* Use this method to mark any types which can have the same instance within it more than once. This will make sure
* the serialised form is stable across multiple serialise-deserialise cycles. Using this on a type with internal cyclic
* references will throw a stack overflow exception during serialisation.
*/
inline fun <reified T : Any> Kryo.noReferencesWithin() {
register(T::class.java, NoReferencesSerializer(getSerializer(T::class.java)))
}
class NoReferencesSerializer<T>(val baseSerializer: Serializer<T>) : Serializer<T>() {
override fun read(kryo: Kryo, input: Input, type: Class<T>): T {
val previousValue = kryo.setReferences(false)
try {
return baseSerializer.read(kryo, input, type)
} finally {
kryo.references = previousValue
}
}
override fun write(kryo: Kryo, output: Output, obj: T) {
val previousValue = kryo.setReferences(false)
try {
baseSerializer.write(kryo, output, obj)
} finally {
kryo.references = previousValue
}
}
}
/**
* Improvement to the builtin JavaSerializer by honouring the [Kryo.getReferences] setting.
*/
object ReferencesAwareJavaSerializer : JavaSerializer() {
override fun write(kryo: Kryo, output: Output, obj: Any) {
if (kryo.references) {
super.write(kryo, output, obj)
}
else {
ObjectOutputStream(output).use {
it.writeObject(obj)
}
}
}
override fun read(kryo: Kryo, input: Input, type: Class<Any>): Any {
return if (kryo.references) {
super.read(kryo, input, type)
}
else {
ObjectInputStream(input).use {
it.readObject()
}
}
} }
} }

View File

@ -1,12 +1,10 @@
package com.r3corda.core.serialization package com.r3corda.core.serialization
import com.esotericsoftware.kryo.DefaultSerializer
import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.KryoException import com.esotericsoftware.kryo.KryoException
import com.esotericsoftware.kryo.Serializer import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.io.Output
import java.lang.ref.WeakReference
import java.util.* import java.util.*
/** /**
@ -23,76 +21,113 @@ import java.util.*
* they are serialized because they have a lot of internal state that does not serialize (well). * they are serialized because they have a lot of internal state that does not serialize (well).
* *
* This models a similar pattern to the readReplace/writeReplace methods in Java serialization. * This models a similar pattern to the readReplace/writeReplace methods in Java serialization.
*
* With Kryo serialisation, these classes should also annotate themselves with <code>@DefaultSerializer</code>. See below.
*
*/ */
interface SerializeAsToken { interface SerializeAsToken {
val token: SerializationToken fun toToken(context: SerializeAsTokenContext): SerializationToken
} }
/** /**
* This represents a token in the serialized stream for an instance of a type that implements [SerializeAsToken] * This represents a token in the serialized stream for an instance of a type that implements [SerializeAsToken]
*/ */
interface SerializationToken { interface SerializationToken {
fun fromToken(): Any fun fromToken(context: SerializeAsTokenContext): Any
} }
/** /**
* A Kryo serializer for [SerializeAsToken] implementations. * A Kryo serializer for [SerializeAsToken] implementations.
* *
* Annotate the [SerializeAsToken] with <code>@DefaultSerializer(SerializeAsTokenSerializer::class)</code> * This is registered in [createKryo].
*/ */
class SerializeAsTokenSerializer<T : SerializeAsToken> : Serializer<T>() { class SerializeAsTokenSerializer<T : SerializeAsToken> : Serializer<T>() {
override fun write(kryo: Kryo, output: Output, obj: T) { override fun write(kryo: Kryo, output: Output, obj: T) {
kryo.writeClassAndObject(output, obj.token) kryo.writeClassAndObject(output, obj.toToken(getContext(kryo) ?: throw KryoException("Attempt to write a ${SerializeAsToken::class.simpleName} instance of ${obj.javaClass.name} without initialising a context")))
} }
override fun read(kryo: Kryo, input: Input, type: Class<T>): T { override fun read(kryo: Kryo, input: Input, type: Class<T>): T {
val token = (kryo.readClassAndObject(input) as? SerializationToken) ?: throw KryoException("Non-token read for tokenized type: ${type.name}") val token = (kryo.readClassAndObject(input) as? SerializationToken) ?: throw KryoException("Non-token read for tokenized type: ${type.name}")
val fromToken = token.fromToken() val fromToken = token.fromToken(getContext(kryo) ?: throw KryoException("Attempt to read a token for a ${SerializeAsToken::class.simpleName} instance of ${type.name} without initialising a context"))
if (type.isAssignableFrom(fromToken.javaClass)) { if (type.isAssignableFrom(fromToken.javaClass)) {
return type.cast(fromToken) return type.cast(fromToken)
} else { } else {
throw KryoException("Token read did not return tokenized type: ${type.name}") throw KryoException("Token read ($token) did not return expected tokenized type: ${type.name}")
}
}
companion object {
private fun getContext(kryo: Kryo): SerializeAsTokenContext? = kryo.context.get(SerializeAsTokenContext::class.java) as? SerializeAsTokenContext
fun setContext(kryo: Kryo, context: SerializeAsTokenContext) {
kryo.context.put(SerializeAsTokenContext::class.java, context)
}
fun clearContext(kryo: Kryo) {
kryo.context.remove(SerializeAsTokenContext::class.java)
} }
} }
} }
/** /**
* A class representing a [SerializationToken] for some object that is not serializable but can be re-created or looked up * A context for mapping SerializationTokens to/from SerializeAsTokens.
* (when deserialized) via a [String] key. *
* A context is initialised with an object containing all the instances of [SerializeAsToken] to eagerly register all the tokens.
* In our case this can be the [ServiceHub].
*
* Then it is a case of using the companion object methods on [SerializeAsTokenSerializer] to set and clear context as necessary
* on the Kryo instance when serializing to enable/disable tokenization.
*/ */
private data class SerializationStringToken(private val key: String, private val className: String) : SerializationToken { class SerializeAsTokenContext(toBeTokenized: Any, kryo: Kryo = createKryo()) {
internal val tokenToTokenized = HashMap<SerializationToken, SerializeAsToken>()
internal var readOnly = false
constructor(key: String, toBeProxied: SerializeAsStringToken) : this(key, toBeProxied.javaClass.name) { init {
tokenized.put(this, WeakReference(toBeProxied)) /*
* Go ahead and eagerly serialize the object to register all of the tokens in the context.
*
* This results in the toToken() method getting called for any [SerializeAsStringToken] instances which
* are encountered in the object graph as they are serialized by Kryo and will therefore register the token to
* object mapping for those instances. We then immediately set the readOnly flag to stop further adhoc or
* accidental registrations from occuring as these could not be deserialized in a deserialization-first
* scenario if they are not part of this iniital context construction serialization.
*/
SerializeAsTokenSerializer.setContext(kryo, this)
toBeTokenized.serialize(kryo)
SerializeAsTokenSerializer.clearContext(kryo)
readOnly = true
} }
}
/**
* A class representing a [SerializationToken] for some object that is not serializable but can be looked up
* (when deserialized) via just the class name.
*/
data class SingletonSerializationToken private constructor(private val className: String) : SerializationToken {
constructor(toBeTokenized: SerializeAsToken) : this(toBeTokenized.javaClass.name)
override fun fromToken(context: SerializeAsTokenContext): Any = context.tokenToTokenized[this] ?:
throw IllegalStateException("Unable to find tokenized instance of ${className} in context $context")
companion object { companion object {
val tokenized = Collections.synchronizedMap(WeakHashMap<SerializationStringToken, WeakReference<SerializeAsStringToken>>()) fun registerWithContext(token: SingletonSerializationToken, toBeTokenized: SerializeAsToken, context: SerializeAsTokenContext): SerializationToken =
} if (token in context.tokenToTokenized) token else registerNewToken(token, toBeTokenized, context)
override fun fromToken(): Any = tokenized.get(this)?.get() ?: // Only allowable if we are in SerializeAsTokenContext init (readOnly == false)
throw IllegalStateException("Unable to find tokenized instance of ${className} for key $key") private fun registerNewToken(token: SingletonSerializationToken, toBeTokenized: SerializeAsToken, context: SerializeAsTokenContext): SerializationToken {
if (context.readOnly) throw UnsupportedOperationException("Attempt to write token for lazy registered ${toBeTokenized.javaClass.name}. " +
"All tokens should be registered during context construction.")
context.tokenToTokenized[token] = toBeTokenized
return token
}
}
} }
/** /**
* A base class for implementing large objects / components / services that need to serialize themselves to a string token * A base class for implementing large objects / components / services that need to serialize themselves to a string token
* to indicate which instance the token is a serialized form of. * to indicate which instance the token is a serialized form of.
*
* This class will also double check that the class is annotated for Kryo serialization. Note it does this on every
* instance constructed but given this is designed to represent heavyweight services or components, this should not be significant.
*/ */
abstract class SerializeAsStringToken(val key: String) : SerializeAsToken { abstract class SingletonSerializeAsToken() : SerializeAsToken {
init { private val token = SingletonSerializationToken(this)
// Verify we have the annotation
val annotation = javaClass.getAnnotation(DefaultSerializer::class.java)
if (annotation == null || annotation.value.java.name != SerializeAsTokenSerializer::class.java.name) {
throw IllegalStateException("${this.javaClass.name} is not annotated with @${DefaultSerializer::class.java.simpleName} set to ${SerializeAsTokenSerializer::class.java.simpleName}")
}
}
override val token: SerializationToken = SerializationStringToken(key, this) override fun toToken(context: SerializeAsTokenContext) = SingletonSerializationToken.registerWithContext(token, this, context)
} }

View File

@ -119,25 +119,7 @@ class ProgressTracker(vararg steps: Step) {
* Writable map that lets you insert child [ProgressTracker]s for particular steps. It's OK to edit this even * Writable map that lets you insert child [ProgressTracker]s for particular steps. It's OK to edit this even
* after a progress tracker has been started. * after a progress tracker has been started.
*/ */
var childrenFor = object : HashMap<Step, ProgressTracker>() { val childrenFor: ChildrenProgressTrackers = ChildrenProgressTrackersImpl()
override fun put(key: Step, value: ProgressTracker): ProgressTracker? {
val r = super.put(key, value)
childSubscriptions[value] = value.changes.subscribe({ _changes.onNext(it) }, { _changes.onError(it) })
value.parent = this@ProgressTracker
_changes.onNext(Change.Structural(this@ProgressTracker, key))
return r
}
override fun remove(key: Step): ProgressTracker? {
val tracker = this[key]
if (tracker != null) {
tracker.parent = null
childSubscriptions[tracker]?.let { it.unsubscribe(); childSubscriptions.remove(tracker) }
}
_changes.onNext(Change.Structural(this@ProgressTracker, key))
return super.remove(key)
}
}
/** The parent of this tracker: set automatically by the parent when a tracker is added as a child */ /** The parent of this tracker: set automatically by the parent when a tracker is added as a child */
var parent: ProgressTracker? = null var parent: ProgressTracker? = null
@ -150,8 +132,6 @@ class ProgressTracker(vararg steps: Step) {
return cursor return cursor
} }
private val childSubscriptions = HashMap<ProgressTracker, Subscription>()
private fun _allSteps(level: Int = 0): List<Pair<Int, Step>> { private fun _allSteps(level: Int = 0): List<Pair<Int, Step>> {
val result = ArrayList<Pair<Int, Step>>() val result = ArrayList<Pair<Int, Step>>()
for (step in steps) { for (step in steps) {
@ -188,4 +168,37 @@ class ProgressTracker(vararg steps: Step) {
* if a step changed its label or rendering). * if a step changed its label or rendering).
*/ */
val changes: Observable<Change> get() = _changes val changes: Observable<Change> get() = _changes
// TODO remove this interface and add its three methods directly into ProgressTracker
interface ChildrenProgressTrackers {
operator fun get(step: ProgressTracker.Step): ProgressTracker?
operator fun set(step: ProgressTracker.Step, childProgressTracker: ProgressTracker)
fun remove(step: ProgressTracker.Step)
} }
private inner class ChildrenProgressTrackersImpl : ChildrenProgressTrackers {
private val map = HashMap<Step, Pair<ProgressTracker, Subscription>>()
override fun get(step: Step): ProgressTracker? = map[step]?.first
override fun set(step: Step, childProgressTracker: ProgressTracker) {
val subscription = childProgressTracker.changes.subscribe({ _changes.onNext(it) }, { _changes.onError(it) })
map[step] = Pair(childProgressTracker, subscription)
childProgressTracker.parent = this@ProgressTracker
_changes.onNext(Change.Structural(this@ProgressTracker, step))
}
override fun remove(step: Step) {
map.remove(step)?.let {
it.first.parent = null
it.second.unsubscribe()
}
_changes.onNext(Change.Structural(this@ProgressTracker, step))
}
}
}

View File

@ -1,21 +1,31 @@
package com.r3corda.protocols package com.r3corda.protocols
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.r3corda.core.crypto.Party
import com.r3corda.core.contracts.TimestampCommand import com.r3corda.core.contracts.TimestampCommand
import com.r3corda.core.contracts.WireTransaction import com.r3corda.core.contracts.WireTransaction
import com.r3corda.core.crypto.DigitalSignature import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SignedData import com.r3corda.core.crypto.SignedData
import com.r3corda.core.crypto.signWithECDSA
import com.r3corda.core.messaging.SingleMessageRecipient import com.r3corda.core.messaging.SingleMessageRecipient
import com.r3corda.core.node.NodeInfo import com.r3corda.core.node.NodeInfo
import com.r3corda.core.node.services.TimestampChecker
import com.r3corda.core.node.services.UniquenessException
import com.r3corda.core.node.services.UniquenessProvider import com.r3corda.core.node.services.UniquenessProvider
import com.r3corda.core.noneOrSingle
import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.random63BitValue import com.r3corda.core.random63BitValue
import com.r3corda.core.serialization.SerializedBytes import com.r3corda.core.serialization.SerializedBytes
import com.r3corda.core.serialization.deserialize
import com.r3corda.core.serialization.serialize
import com.r3corda.core.utilities.ProgressTracker import com.r3corda.core.utilities.ProgressTracker
import com.r3corda.core.utilities.UntrustworthyData import com.r3corda.core.utilities.UntrustworthyData
import java.security.PublicKey import java.security.PublicKey
object NotaryProtocol {
val TOPIC = "platform.notary.request"
val TOPIC_INITIATE = "platform.notary.initiate"
/** /**
* A protocol to be used for obtaining a signature from a [NotaryService] ascertaining the transaction * A protocol to be used for obtaining a signature from a [NotaryService] ascertaining the transaction
* timestamp is correct and none of its inputs have been used in another completed transaction * timestamp is correct and none of its inputs have been used in another completed transaction
@ -23,10 +33,9 @@ import java.security.PublicKey
* @throws NotaryException in case the any of the inputs to the transaction have been consumed * @throws NotaryException in case the any of the inputs to the transaction have been consumed
* by another transaction or the timestamp is invalid * by another transaction or the timestamp is invalid
*/ */
class NotaryProtocol(private val wtx: WireTransaction, class Client(private val wtx: WireTransaction,
override val progressTracker: ProgressTracker = NotaryProtocol.tracker()) : ProtocolLogic<DigitalSignature.LegallyIdentifiable>() { override val progressTracker: ProgressTracker = Client.tracker()) : ProtocolLogic<DigitalSignature.LegallyIdentifiable>() {
companion object { companion object {
val TOPIC = "platform.notary.request"
object REQUESTING : ProgressTracker.Step("Requesting signature by Notary service") object REQUESTING : ProgressTracker.Step("Requesting signature by Notary service")
@ -42,9 +51,14 @@ class NotaryProtocol(private val wtx: WireTransaction,
progressTracker.currentStep = REQUESTING progressTracker.currentStep = REQUESTING
notaryNode = findNotaryNode() notaryNode = findNotaryNode()
val sessionID = random63BitValue() val sendSessionID = random63BitValue()
val request = SignRequest(wtx.serialized, serviceHub.storageService.myLegalIdentity, serviceHub.networkService.myAddress, sessionID) val receiveSessionID = random63BitValue()
val response = sendAndReceive<Result>(TOPIC, notaryNode.address, 0, sessionID, request)
val handshake = Handshake(serviceHub.networkService.myAddress, sendSessionID, receiveSessionID)
sendAndReceive<Unit>(TOPIC_INITIATE, notaryNode.address, 0, receiveSessionID, handshake)
val request = SignRequest(wtx.serialized, serviceHub.storageService.myLegalIdentity)
val response = sendAndReceive<Result>(TOPIC, notaryNode.address, sendSessionID, receiveSessionID, request)
val notaryResult = validateResponse(response) val notaryResult = validateResponse(response)
return notaryResult.sig ?: throw NotaryException(notaryResult.error!!) return notaryResult.sig ?: throw NotaryException(notaryResult.error!!)
@ -84,12 +98,94 @@ class NotaryProtocol(private val wtx: WireTransaction,
val notaryNode = serviceHub.networkMapCache.getNodeByPublicKey(notaryKey) val notaryNode = serviceHub.networkMapCache.getNodeByPublicKey(notaryKey)
return notaryNode ?: throw IllegalStateException("No Notary node can be found with the specified public key") return notaryNode ?: throw IllegalStateException("No Notary node can be found with the specified public key")
} }
}
/**
* Checks that the timestamp command is valid (if present) and commits the input state, or returns a conflict
* if any of the input states have been previously committed.
*
* Extend this class, overriding _beforeCommit_ to add custom transaction processing/validation logic.
*
* TODO: the notary service should only be able to see timestamp commands and inputs
*/
open class Service(val otherSide: SingleMessageRecipient,
val sendSessionID: Long,
val receiveSessionID: Long,
val timestampChecker: TimestampChecker,
val uniquenessProvider: UniquenessProvider) : ProtocolLogic<Unit>() {
@Suspendable
override fun call() {
val request = receive<SignRequest>(TOPIC, receiveSessionID).validate { it }
val txBits = request.txBits
val reqIdentity = request.callerIdentity
val wtx = txBits.deserialize()
val result: Result
try {
validateTimestamp(wtx)
beforeCommit(wtx, reqIdentity)
commitInputStates(wtx, reqIdentity)
val sig = sign(txBits)
result = Result.noError(sig)
} catch(e: NotaryException) {
result = Result.withError(e.error)
}
send(TOPIC, otherSide, sendSessionID, result)
}
private fun validateTimestamp(tx: WireTransaction) {
val timestampCmd = try {
tx.commands.noneOrSingle { it.value is TimestampCommand } ?: return
} catch (e: IllegalArgumentException) {
throw NotaryException(NotaryError.MoreThanOneTimestamp())
}
val myIdentity = serviceHub.storageService.myLegalIdentity
if (!timestampCmd.signers.contains(myIdentity.owningKey))
throw NotaryException(NotaryError.NotForMe())
if (!timestampChecker.isValid(timestampCmd.value as TimestampCommand))
throw NotaryException(NotaryError.TimestampInvalid())
}
/**
* No pre-commit processing is done. Transaction is not checked for contract-validity, as that would require fully
* resolving it into a [TransactionForVerification], for which the caller would have to reveal the whole transaction
* history chain.
* As a result, the Notary _will commit invalid transactions_ as well, but as it also records the identity of
* the caller, it is possible to raise a dispute and verify the validity of the transaction and subsequently
* undo the commit of the input states (the exact mechanism still needs to be worked out)
*/
@Suspendable
open fun beforeCommit(wtx: WireTransaction, reqIdentity: Party) {
}
private fun commitInputStates(tx: WireTransaction, reqIdentity: Party) {
try {
uniquenessProvider.commit(tx, reqIdentity)
} catch (e: UniquenessException) {
val conflictData = e.error.serialize()
val signedConflict = SignedData(conflictData, sign(conflictData))
throw NotaryException(NotaryError.Conflict(tx, signedConflict))
}
}
private fun <T : Any> sign(bits: SerializedBytes<T>): DigitalSignature.LegallyIdentifiable {
val mySigningKey = serviceHub.storageService.myLegalIdentityKey
val myIdentity = serviceHub.storageService.myLegalIdentity
return mySigningKey.signWithECDSA(bits, myIdentity)
}
}
class Handshake(
replyTo: SingleMessageRecipient,
val sendSessionID: Long,
sessionID: Long) : AbstractRequestMessage(replyTo, sessionID)
/** TODO: The caller must authenticate instead of just specifying its identity */ /** TODO: The caller must authenticate instead of just specifying its identity */
class SignRequest(val txBits: SerializedBytes<WireTransaction>, class SignRequest(val txBits: SerializedBytes<WireTransaction>,
val callerIdentity: Party, val callerIdentity: Party)
replyTo: SingleMessageRecipient,
sessionID: Long) : AbstractRequestMessage(replyTo, sessionID)
data class Result private constructor(val sig: DigitalSignature.LegallyIdentifiable?, val error: NotaryError?) { data class Result private constructor(val sig: DigitalSignature.LegallyIdentifiable?, val error: NotaryError?) {
companion object { companion object {
@ -97,6 +193,24 @@ class NotaryProtocol(private val wtx: WireTransaction,
fun noError(sig: DigitalSignature.LegallyIdentifiable) = Result(sig, null) fun noError(sig: DigitalSignature.LegallyIdentifiable) = Result(sig, null)
} }
} }
interface Factory {
fun create(otherSide: SingleMessageRecipient,
sendSessionID: Long,
receiveSessionID: Long,
timestampChecker: TimestampChecker,
uniquenessProvider: UniquenessProvider): Service
}
object DefaultFactory : Factory {
override fun create(otherSide: SingleMessageRecipient,
sendSessionID: Long,
receiveSessionID: Long,
timestampChecker: TimestampChecker,
uniquenessProvider: UniquenessProvider): Service {
return Service(otherSide, sendSessionID, receiveSessionID, timestampChecker, uniquenessProvider)
}
}
} }
class NotaryException(val error: NotaryError) : Exception() { class NotaryException(val error: NotaryError) : Exception() {
@ -115,4 +229,6 @@ sealed class NotaryError {
/** Thrown if the time specified in the timestamp command is outside the allowed tolerance */ /** Thrown if the time specified in the timestamp command is outside the allowed tolerance */
class TimestampInvalid : NotaryError() class TimestampInvalid : NotaryError()
class TransactionInvalid : NotaryError()
} }

View File

@ -1,7 +1,6 @@
package com.r3corda.protocols package com.r3corda.protocols
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.r3corda.core.*
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.messaging.SingleMessageRecipient import com.r3corda.core.messaging.SingleMessageRecipient

View File

@ -158,7 +158,7 @@ object TwoPartyDealProtocol {
@Suspendable @Suspendable
private fun getNotarySignature(stx: SignedTransaction): DigitalSignature.LegallyIdentifiable { private fun getNotarySignature(stx: SignedTransaction): DigitalSignature.LegallyIdentifiable {
progressTracker.currentStep = NOTARY progressTracker.currentStep = NOTARY
return subProtocol(NotaryProtocol(stx.tx)) return subProtocol(NotaryProtocol.Client(stx.tx))
} }
open fun signWithOurKey(partialTX: SignedTransaction): DigitalSignature.WithKey { open fun signWithOurKey(partialTX: SignedTransaction): DigitalSignature.WithKey {

View File

@ -0,0 +1,48 @@
package com.r3corda.protocols
import co.paralleluniverse.fibers.Suspendable
import com.r3corda.core.contracts.TransactionVerificationException
import com.r3corda.core.contracts.WireTransaction
import com.r3corda.core.contracts.toLedgerTransaction
import com.r3corda.core.crypto.Party
import com.r3corda.core.messaging.SingleMessageRecipient
import com.r3corda.core.node.services.TimestampChecker
import com.r3corda.core.node.services.UniquenessProvider
import java.security.SignatureException
/**
* A notary commit protocol that makes sure a given transaction is valid before committing it. This does mean that the calling
* party has to reveal the whole transaction history; however, we avoid complex conflict resolution logic where a party
* has its input states "blocked" by a transaction from another party, and needs to establish whether that transaction was
* indeed valid
*/
class ValidatingNotaryProtocol(otherSide: SingleMessageRecipient,
sessionIdForSend: Long,
sessionIdForReceive: Long,
timestampChecker: TimestampChecker,
uniquenessProvider: UniquenessProvider) : NotaryProtocol.Service(otherSide, sessionIdForSend, sessionIdForReceive, timestampChecker, uniquenessProvider) {
@Suspendable
override fun beforeCommit(wtx: WireTransaction, reqIdentity: Party) {
try {
validateDependencies(reqIdentity, wtx)
checkContractValid(wtx)
} catch (e: Exception) {
when (e) {
is TransactionVerificationException,
is SignatureException -> throw NotaryException(NotaryError.TransactionInvalid())
else -> throw e
}
}
}
private fun checkContractValid(wtx: WireTransaction) {
val ltx = wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
serviceHub.verifyTransaction(ltx)
}
@Suspendable
private fun validateDependencies(reqIdentity: Party, wtx: WireTransaction) {
val otherSide = serviceHub.networkMapCache.getNodeByPublicKey(reqIdentity.owningKey)!!.address
subProtocol(ResolveTransactionsProtocol(wtx, otherSide))
}
}

View File

@ -1,34 +1,68 @@
package com.r3corda.core.serialization package com.r3corda.core.serialization
import com.esotericsoftware.kryo.Kryo import com.google.common.primitives.Ints
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test import org.junit.Test
import java.time.Instant import java.time.Instant
import kotlin.test.assertEquals import java.util.*
import kotlin.test.assertNull
class KryoTests { class KryoTests {
data class Person(val name: String, val birthday: Instant?)
private val kryo: Kryo = createKryo() private val kryo = createKryo()
@Test @Test
fun ok() { fun ok() {
val april_17th = Instant.parse("1984-04-17T00:30:00.00Z") val birthday = Instant.parse("1984-04-17T00:30:00.00Z")
val mike = Person("mike", april_17th) val mike = Person("mike", birthday)
val bits = mike.serialize(kryo) val bits = mike.serialize(kryo)
with(bits.deserialize<Person>(kryo)) { assertThat(bits.deserialize(kryo)).isEqualTo(Person("mike", birthday))
assertEquals("mike", name)
assertEquals(april_17th, birthday)
}
} }
@Test @Test
fun nullables() { fun nullables() {
val bob = Person("bob", null) val bob = Person("bob", null)
val bits = bob.serialize(kryo) val bits = bob.serialize(kryo)
with(bits.deserialize<Person>(kryo)) { assertThat(bits.deserialize(kryo)).isEqualTo(Person("bob", null))
assertEquals("bob", name)
assertNull(birthday)
} }
@Test
fun `serialised form is stable when the same object instance is added to the deserialised object graph`() {
kryo.noReferencesWithin<ArrayList<*>>()
val obj = Ints.toByteArray(0x01234567).opaque()
val originalList = arrayListOf(obj)
val deserialisedList = originalList.serialize(kryo).deserialize(kryo)
originalList += obj
deserialisedList += obj
assertThat(deserialisedList.serialize(kryo)).isEqualTo(originalList.serialize(kryo))
} }
@Test
fun `serialised form is stable when the same object instance occurs more than once, and using java serialisation`() {
kryo.noReferencesWithin<ArrayList<*>>()
val instant = Instant.ofEpochMilli(123)
val instantCopy = Instant.ofEpochMilli(123)
assertThat(instant).isNotSameAs(instantCopy)
val listWithCopies = arrayListOf(instant, instantCopy)
val listWithSameInstances = arrayListOf(instant, instant)
assertThat(listWithSameInstances.serialize(kryo)).isEqualTo(listWithCopies.serialize(kryo))
}
@Test
fun `cyclic object graph`() {
val cyclic = Cyclic(3)
val bits = cyclic.serialize(kryo)
assertThat(bits.deserialize(kryo)).isEqualTo(cyclic)
}
private data class Person(val name: String, val birthday: Instant?)
@Suppress("unused")
private class Cyclic(val value: Int) {
val thisInstance = this
override fun equals(other: Any?): Boolean = (this === other) || (other is Cyclic && this.value == other.value)
override fun hashCode(): Int = value.hashCode()
override fun toString(): String = "Cyclic($value)"
}
} }

View File

@ -1,17 +1,34 @@
package com.r3corda.core.serialization package com.r3corda.core.serialization
import com.esotericsoftware.kryo.DefaultSerializer import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.KryoException
import com.esotericsoftware.kryo.io.Output
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Test import org.junit.Test
import kotlin.test.assertEquals import java.io.ByteArrayOutputStream
import kotlin.test.assertNotEquals
class SerializationTokenTest { class SerializationTokenTest {
lateinit var kryo: Kryo
@Before
fun setup() {
kryo = THREAD_LOCAL_KRYO.get()
}
@After
fun cleanup() {
SerializeAsTokenSerializer.clearContext(kryo)
}
// Large tokenizable object so we can tell from the smaller number of serialized bytes it was actually tokenized // Large tokenizable object so we can tell from the smaller number of serialized bytes it was actually tokenized
@DefaultSerializer(SerializeAsTokenSerializer::class) private class LargeTokenizable : SingletonSerializeAsToken() {
private class LargeTokenizable(size: Int) : SerializeAsStringToken(size.toString()) { val bytes = OpaqueBytes(ByteArray(1024))
val bytes = OpaqueBytes(ByteArray(size))
val numBytes: Int
get() = bytes.size
override fun hashCode() = bytes.bits.size override fun hashCode() = bytes.bits.size
@ -20,61 +37,78 @@ class SerializationTokenTest {
@Test @Test
fun `write token and read tokenizable`() { fun `write token and read tokenizable`() {
val numBytes = 1024 val tokenizableBefore = LargeTokenizable()
val tokenizableBefore = LargeTokenizable(numBytes) val context = SerializeAsTokenContext(tokenizableBefore, kryo)
val serializedBytes = tokenizableBefore.serialize() SerializeAsTokenSerializer.setContext(kryo, context)
assertThat(serializedBytes.size).isLessThan(numBytes) val serializedBytes = tokenizableBefore.serialize(kryo)
val tokenizableAfter = serializedBytes.deserialize() assertThat(serializedBytes.size).isLessThan(tokenizableBefore.numBytes)
assertEquals(tokenizableBefore, tokenizableAfter) val tokenizableAfter = serializedBytes.deserialize(kryo)
}
@Test
fun `check same sized tokenizable equal`() {
val tokenizableBefore = LargeTokenizable(1024)
val tokenizableAfter = LargeTokenizable(1024)
assertEquals(tokenizableBefore, tokenizableAfter)
}
@Test
fun `check different sized tokenizable not equal`() {
val tokenizableBefore = LargeTokenizable(1024)
val tokenizableAfter = LargeTokenizable(1025)
assertNotEquals(tokenizableBefore, tokenizableAfter)
}
@DefaultSerializer(SerializeAsTokenSerializer::class)
private class IntegerSerializeAsKeyedToken(val value: Int) : SerializeAsStringToken(value.toString())
@Test
fun `write and read keyed`() {
val tokenizableBefore1 = IntegerSerializeAsKeyedToken(123)
val tokenizableBefore2 = IntegerSerializeAsKeyedToken(456)
val serializedBytes1 = tokenizableBefore1.serialize()
val tokenizableAfter1 = serializedBytes1.deserialize()
val serializedBytes2 = tokenizableBefore2.serialize()
val tokenizableAfter2 = serializedBytes2.deserialize()
assertThat(tokenizableAfter1).isSameAs(tokenizableBefore1)
assertThat(tokenizableAfter2).isSameAs(tokenizableBefore2)
}
@DefaultSerializer(SerializeAsTokenSerializer::class)
private class UnitSerializeAsSingletonToken : SerializeAsStringToken("Unit0")
@Test
fun `write and read singleton`() {
val tokenizableBefore = UnitSerializeAsSingletonToken()
val serializedBytes = tokenizableBefore.serialize()
val tokenizableAfter = serializedBytes.deserialize()
assertThat(tokenizableAfter).isSameAs(tokenizableBefore) assertThat(tokenizableAfter).isSameAs(tokenizableBefore)
} }
private class UnannotatedSerializeAsSingletonToken : SerializeAsStringToken("Unannotated0") private class UnitSerializeAsToken : SingletonSerializeAsToken()
@Test(expected = IllegalStateException::class) @Test
fun `unannotated throws`() { fun `write and read singleton`() {
@Suppress("UNUSED_VARIABLE") val tokenizableBefore = UnitSerializeAsToken()
val tokenizableBefore = UnannotatedSerializeAsSingletonToken() val context = SerializeAsTokenContext(tokenizableBefore, kryo)
SerializeAsTokenSerializer.setContext(kryo, context)
val serializedBytes = tokenizableBefore.serialize(kryo)
val tokenizableAfter = serializedBytes.deserialize(kryo)
assertThat(tokenizableAfter).isSameAs(tokenizableBefore)
}
@Test(expected = UnsupportedOperationException::class)
fun `new token encountered after context init`() {
val tokenizableBefore = UnitSerializeAsToken()
val context = SerializeAsTokenContext(emptyList<Any>(), kryo)
SerializeAsTokenSerializer.setContext(kryo, context)
tokenizableBefore.serialize(kryo)
}
@Test(expected = UnsupportedOperationException::class)
fun `deserialize unregistered token`() {
val tokenizableBefore = UnitSerializeAsToken()
val context = SerializeAsTokenContext(emptyList<Any>(), kryo)
SerializeAsTokenSerializer.setContext(kryo, context)
val serializedBytes = tokenizableBefore.toToken(SerializeAsTokenContext(emptyList<Any>(), kryo)).serialize(kryo)
serializedBytes.deserialize(kryo)
}
@Test(expected = KryoException::class)
fun `no context set`() {
val tokenizableBefore = UnitSerializeAsToken()
tokenizableBefore.serialize(kryo)
}
@Test(expected = KryoException::class)
fun `deserialize non-token`() {
val tokenizableBefore = UnitSerializeAsToken()
val context = SerializeAsTokenContext(tokenizableBefore, kryo)
SerializeAsTokenSerializer.setContext(kryo, context)
val stream = ByteArrayOutputStream()
Output(stream).use {
kryo.writeClass(it, SingletonSerializeAsToken::class.java)
kryo.writeObject(it, emptyList<Any>())
}
val serializedBytes = SerializedBytes<Any>(stream.toByteArray())
serializedBytes.deserialize(kryo)
}
private class WrongTypeSerializeAsToken : SerializeAsToken {
override fun toToken(context: SerializeAsTokenContext): SerializationToken {
return object : SerializationToken {
override fun fromToken(context: SerializeAsTokenContext): Any = UnitSerializeAsToken()
}
}
}
@Test(expected = KryoException::class)
fun `token returns unexpected type`() {
val tokenizableBefore = WrongTypeSerializeAsToken()
val context = SerializeAsTokenContext(tokenizableBefore, kryo)
SerializeAsTokenSerializer.setContext(kryo, context)
val serializedBytes = tokenizableBefore.serialize(kryo)
serializedBytes.deserialize(kryo)
} }
} }

View File

@ -10,6 +10,8 @@ Here are changes in git master that haven't yet made it to a snapshot release:
* The cash contract has moved from com.r3corda.contracts to com.r3corda.contracts.cash. * The cash contract has moved from com.r3corda.contracts to com.r3corda.contracts.cash.
* Amount class is now generic, to support non-currency types (such as assets, or currency with additional information). * Amount class is now generic, to support non-currency types (such as assets, or currency with additional information).
* Refactored the Cash contract to have a new FungibleAsset superclass, to model all countable assets that can be merged
and split (currency, barrels of oil, etc.)
Milestone 0 Milestone 0

View File

@ -20,9 +20,6 @@ import com.r3corda.node.services.api.AcceptsFileUpload
import com.r3corda.node.services.api.CheckpointStorage import com.r3corda.node.services.api.CheckpointStorage
import com.r3corda.node.services.api.MonitoringService import com.r3corda.node.services.api.MonitoringService
import com.r3corda.node.services.api.ServiceHubInternal import com.r3corda.node.services.api.ServiceHubInternal
import com.r3corda.node.services.transactions.InMemoryUniquenessProvider
import com.r3corda.node.services.transactions.NotaryService
import com.r3corda.node.services.transactions.TimestampChecker
import com.r3corda.node.services.clientapi.NodeInterestRates import com.r3corda.node.services.clientapi.NodeInterestRates
import com.r3corda.node.services.config.NodeConfiguration import com.r3corda.node.services.config.NodeConfiguration
import com.r3corda.node.services.identity.InMemoryIdentityService import com.r3corda.node.services.identity.InMemoryIdentityService
@ -36,6 +33,10 @@ import com.r3corda.node.services.persistence.NodeAttachmentService
import com.r3corda.node.services.persistence.PerFileCheckpointStorage import com.r3corda.node.services.persistence.PerFileCheckpointStorage
import com.r3corda.node.services.persistence.StorageServiceImpl import com.r3corda.node.services.persistence.StorageServiceImpl
import com.r3corda.node.services.statemachine.StateMachineManager import com.r3corda.node.services.statemachine.StateMachineManager
import com.r3corda.node.services.transactions.InMemoryUniquenessProvider
import com.r3corda.node.services.transactions.NotaryService
import com.r3corda.node.services.transactions.SimpleNotaryService
import com.r3corda.node.services.transactions.ValidatingNotaryService
import com.r3corda.node.services.wallet.NodeWalletService import com.r3corda.node.services.wallet.NodeWalletService
import com.r3corda.node.utilities.AddOrRemove import com.r3corda.node.utilities.AddOrRemove
import com.r3corda.node.utilities.AffinityExecutor import com.r3corda.node.utilities.AffinityExecutor
@ -87,7 +88,7 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
override val keyManagementService: KeyManagementService get() = keyManagement override val keyManagementService: KeyManagementService get() = keyManagement
override val identityService: IdentityService get() = identity override val identityService: IdentityService get() = identity
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
override val clock: Clock get() = platformClock override val clock: Clock = platformClock
} }
val info: NodeInfo by lazy { val info: NodeInfo by lazy {
@ -106,6 +107,8 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
lateinit var identity: IdentityService lateinit var identity: IdentityService
lateinit var net: MessagingService lateinit var net: MessagingService
lateinit var api: APIServer lateinit var api: APIServer
var isPreviousCheckpointsPresent = false
private set
/** Completes once the node has successfully registered with the network map service. Null until [start] returns. */ /** Completes once the node has successfully registered with the network map service. Null until [start] returns. */
@Volatile var networkMapRegistrationFuture: ListenableFuture<Unit>? = null @Volatile var networkMapRegistrationFuture: ListenableFuture<Unit>? = null
@ -123,27 +126,35 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
storage = storageServices.first storage = storageServices.first
checkpointStorage = storageServices.second checkpointStorage = storageServices.second
net = makeMessagingService() net = makeMessagingService()
smm = StateMachineManager(services, checkpointStorage, serverThread)
wallet = NodeWalletService(services) wallet = NodeWalletService(services)
keyManagement = E2ETestKeyManagementService() keyManagement = E2ETestKeyManagementService()
makeInterestRatesOracleService() makeInterestRatesOracleService()
api = APIServerImpl(this)
// Build services we're advertising
if (NetworkMapService.Type in info.advertisedServices) makeNetworkMapService()
if (NotaryService.Type in info.advertisedServices) makeNotaryService()
identity = makeIdentityService() identity = makeIdentityService()
api = APIServerImpl(this)
smm = StateMachineManager(services, listOf(storage, net, wallet, keyManagement, identity, platformClock), checkpointStorage, serverThread)
// This object doesn't need to be referenced from this class because it registers handlers on the network // This object doesn't need to be referenced from this class because it registers handlers on the network
// service and so that keeps it from being collected. // service and so that keeps it from being collected.
DataVendingService(net, storage) DataVendingService(net, storage)
buildAdvertisedServices()
startMessagingService() startMessagingService()
networkMapRegistrationFuture = registerWithNetworkMap() networkMapRegistrationFuture = registerWithNetworkMap()
isPreviousCheckpointsPresent = checkpointStorage.checkpoints.any()
smm.start()
started = true started = true
return this return this
} }
private fun buildAdvertisedServices() {
val serviceTypes = info.advertisedServices
if (NetworkMapService.Type in serviceTypes) makeNetworkMapService()
val notaryServiceType = serviceTypes.singleOrNull { it.isSubTypeOf(NotaryService.Type) }
if (notaryServiceType != null) makeNotaryService(notaryServiceType)
}
/** /**
* Register this node with the network map cache, and load network map from a remote service (and register for * Register this node with the network map cache, and load network map from a remote service (and register for
* updates) if one has been supplied. * updates) if one has been supplied.
@ -197,10 +208,15 @@ abstract class AbstractNode(val dir: Path, val configuration: NodeConfiguration,
inNodeNetworkMapService = InMemoryNetworkMapService(net, reg, services.networkMapCache) inNodeNetworkMapService = InMemoryNetworkMapService(net, reg, services.networkMapCache)
} }
open protected fun makeNotaryService() { open protected fun makeNotaryService(type: ServiceType) {
val uniquenessProvider = InMemoryUniquenessProvider() val uniquenessProvider = InMemoryUniquenessProvider()
val timestampChecker = TimestampChecker(platformClock, 30.seconds) val timestampChecker = TimestampChecker(platformClock, 30.seconds)
inNodeNotaryService = NotaryService(net, storage.myLegalIdentity, storage.myLegalIdentityKey, uniquenessProvider, timestampChecker)
inNodeNotaryService = when (type) {
is SimpleNotaryService.Type -> SimpleNotaryService(smm, net, timestampChecker, uniquenessProvider)
is ValidatingNotaryService.Type -> ValidatingNotaryService(smm, net, timestampChecker, uniquenessProvider)
else -> null
}
} }
lateinit var interestRatesService: NodeInterestRates.Service lateinit var interestRatesService: NodeInterestRates.Service

View File

@ -7,6 +7,7 @@ import com.r3corda.core.node.NodeInfo
import com.r3corda.core.node.services.ServiceType import com.r3corda.core.node.services.ServiceType
import com.r3corda.core.utilities.loggerFor import com.r3corda.core.utilities.loggerFor
import com.r3corda.node.api.APIServer import com.r3corda.node.api.APIServer
import com.r3corda.node.serialization.NodeClock
import com.r3corda.node.services.config.NodeConfiguration import com.r3corda.node.services.config.NodeConfiguration
import com.r3corda.node.services.messaging.ArtemisMessagingService import com.r3corda.node.services.messaging.ArtemisMessagingService
import com.r3corda.node.servlets.AttachmentDownloadServlet import com.r3corda.node.servlets.AttachmentDownloadServlet
@ -52,7 +53,7 @@ class ConfigurationException(message: String) : Exception(message)
*/ */
class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration, class Node(dir: Path, val p2pAddr: HostAndPort, configuration: NodeConfiguration,
networkMapAddress: NodeInfo?, advertisedServices: Set<ServiceType>, networkMapAddress: NodeInfo?, advertisedServices: Set<ServiceType>,
clock: Clock = Clock.systemUTC(), clock: Clock = NodeClock(),
val clientAPIs: List<Class<*>> = listOf()) : AbstractNode(dir, configuration, networkMapAddress, advertisedServices, clock) { val clientAPIs: List<Class<*>> = listOf()) : AbstractNode(dir, configuration, networkMapAddress, advertisedServices, clock) {
companion object { companion object {
/** The port that is used by default if none is specified. As you know, 31337 is the most elite number. */ /** The port that is used by default if none is specified. As you know, 31337 is the most elite number. */

View File

@ -12,16 +12,16 @@ import com.r3corda.core.node.services.ServiceType
import com.r3corda.core.node.services.testing.MockIdentityService import com.r3corda.core.node.services.testing.MockIdentityService
import com.r3corda.core.utilities.loggerFor import com.r3corda.core.utilities.loggerFor
import com.r3corda.node.internal.AbstractNode import com.r3corda.node.internal.AbstractNode
import com.r3corda.node.serialization.NodeClock
import com.r3corda.node.services.config.NodeConfiguration import com.r3corda.node.services.config.NodeConfiguration
import com.r3corda.node.services.network.InMemoryMessagingNetwork import com.r3corda.node.services.network.InMemoryMessagingNetwork
import com.r3corda.node.services.network.NetworkMapService import com.r3corda.node.services.network.NetworkMapService
import com.r3corda.node.services.transactions.NotaryService import com.r3corda.node.services.transactions.SimpleNotaryService
import com.r3corda.node.utilities.AffinityExecutor import com.r3corda.node.utilities.AffinityExecutor
import org.slf4j.Logger import org.slf4j.Logger
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.security.KeyPair import java.security.KeyPair
import java.time.Clock
import java.util.* import java.util.*
/** /**
@ -66,7 +66,7 @@ class MockNetwork(private val threadPerNode: Boolean = false,
} }
open class MockNode(dir: Path, config: NodeConfiguration, val mockNet: MockNetwork, networkMapAddr: NodeInfo?, open class MockNode(dir: Path, config: NodeConfiguration, val mockNet: MockNetwork, networkMapAddr: NodeInfo?,
advertisedServices: Set<ServiceType>, val id: Int, val keyPair: KeyPair?) : AbstractNode(dir, config, networkMapAddr, advertisedServices, Clock.systemUTC()) { advertisedServices: Set<ServiceType>, val id: Int, val keyPair: KeyPair?) : AbstractNode(dir, config, networkMapAddr, advertisedServices, NodeClock()) {
override val log: Logger = loggerFor<MockNode>() override val log: Logger = loggerFor<MockNode>()
override val serverThread: AffinityExecutor = override val serverThread: AffinityExecutor =
if (mockNet.threadPerNode) if (mockNet.threadPerNode)
@ -149,12 +149,12 @@ class MockNetwork(private val threadPerNode: Boolean = false,
fun createTwoNodes(nodeFactory: Factory = defaultFactory, notaryKeyPair: KeyPair? = null): Pair<MockNode, MockNode> { fun createTwoNodes(nodeFactory: Factory = defaultFactory, notaryKeyPair: KeyPair? = null): Pair<MockNode, MockNode> {
require(nodes.isEmpty()) require(nodes.isEmpty())
return Pair( return Pair(
createNode(null, -1, nodeFactory, true, null, notaryKeyPair, NetworkMapService.Type, NotaryService.Type), createNode(null, -1, nodeFactory, true, null, notaryKeyPair, NetworkMapService.Type, SimpleNotaryService.Type),
createNode(nodes[0].info, -1, nodeFactory, true, null) createNode(nodes[0].info, -1, nodeFactory, true, null)
) )
} }
fun createNotaryNode(legalName: String? = null, keyPair: KeyPair? = null) = createNode(null, -1, defaultFactory, true, legalName, keyPair, NetworkMapService.Type, NotaryService.Type) fun createNotaryNode(legalName: String? = null, keyPair: KeyPair? = null) = createNode(null, -1, defaultFactory, true, legalName, keyPair, NetworkMapService.Type, SimpleNotaryService.Type)
fun createPartyNode(networkMapAddr: NodeInfo, legalName: String? = null, keyPair: KeyPair? = null) = createNode(networkMapAddr, -1, defaultFactory, true, legalName, keyPair) fun createPartyNode(networkMapAddr: NodeInfo, legalName: String? = null, keyPair: KeyPair? = null) = createNode(networkMapAddr, -1, defaultFactory, true, legalName, keyPair)
fun addressToNode(address: SingleMessageRecipient): MockNode = nodes.single { it.net.myAddress == address } fun addressToNode(address: SingleMessageRecipient): MockNode = nodes.single { it.net.myAddress == address }

View File

@ -9,11 +9,11 @@ import com.r3corda.core.node.services.ServiceType
import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.then import com.r3corda.core.then
import com.r3corda.core.utilities.ProgressTracker import com.r3corda.core.utilities.ProgressTracker
import com.r3corda.node.services.transactions.NotaryService
import com.r3corda.node.services.clientapi.NodeInterestRates import com.r3corda.node.services.clientapi.NodeInterestRates
import com.r3corda.node.services.config.NodeConfiguration import com.r3corda.node.services.config.NodeConfiguration
import com.r3corda.node.services.network.InMemoryMessagingNetwork import com.r3corda.node.services.network.InMemoryMessagingNetwork
import com.r3corda.node.services.network.NetworkMapService import com.r3corda.node.services.network.NetworkMapService
import com.r3corda.node.services.transactions.SimpleNotaryService
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import java.nio.file.Path import java.nio.file.Path
@ -82,7 +82,7 @@ abstract class Simulation(val runAsync: Boolean,
object NotaryNodeFactory : MockNetwork.Factory { object NotaryNodeFactory : MockNetwork.Factory {
override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?, override fun create(dir: Path, config: NodeConfiguration, network: MockNetwork, networkMapAddr: NodeInfo?,
advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode { advertisedServices: Set<ServiceType>, id: Int, keyPair: KeyPair?): MockNetwork.MockNode {
require(advertisedServices.contains(NotaryService.Type)) require(advertisedServices.contains(SimpleNotaryService.Type))
val cfg = object : NodeConfiguration { val cfg = object : NodeConfiguration {
override val myLegalName: String = "Notary Service" override val myLegalName: String = "Notary Service"
override val exportJMXto: String = "" override val exportJMXto: String = ""
@ -134,7 +134,7 @@ abstract class Simulation(val runAsync: Boolean,
val networkMap: SimulatedNode val networkMap: SimulatedNode
= network.createNode(null, nodeFactory = NetworkMapNodeFactory, advertisedServices = NetworkMapService.Type) as SimulatedNode = network.createNode(null, nodeFactory = NetworkMapNodeFactory, advertisedServices = NetworkMapService.Type) as SimulatedNode
val notary: SimulatedNode val notary: SimulatedNode
= network.createNode(networkMap.info, nodeFactory = NotaryNodeFactory, advertisedServices = NotaryService.Type) as SimulatedNode = network.createNode(networkMap.info, nodeFactory = NotaryNodeFactory, advertisedServices = SimpleNotaryService.Type) as SimulatedNode
val regulators: List<SimulatedNode> = listOf(network.createNode(networkMap.info, start = false, nodeFactory = RegulatorFactory) as SimulatedNode) val regulators: List<SimulatedNode> = listOf(network.createNode(networkMap.info, start = false, nodeFactory = RegulatorFactory) as SimulatedNode)
val ratesOracle: SimulatedNode val ratesOracle: SimulatedNode
= network.createNode(networkMap.info, start = false, nodeFactory = RatesOracleFactory, advertisedServices = NodeInterestRates.Type) as SimulatedNode = network.createNode(networkMap.info, start = false, nodeFactory = RatesOracleFactory, advertisedServices = NodeInterestRates.Type) as SimulatedNode

View File

@ -1,20 +1,29 @@
@file:Suppress("UNUSED_PARAMETER") package com.r3corda.node.internal.testing
package com.r3corda.node.testutils
import com.r3corda.contracts.DummyContract import com.r3corda.contracts.DummyContract
import com.r3corda.core.contracts.StateRef import com.r3corda.core.contracts.StateRef
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.seconds
import com.r3corda.core.testing.DUMMY_NOTARY import com.r3corda.core.testing.DUMMY_NOTARY
import com.r3corda.core.testing.DUMMY_NOTARY_KEY import com.r3corda.core.testing.DUMMY_NOTARY_KEY
import com.r3corda.node.internal.AbstractNode import com.r3corda.node.internal.AbstractNode
import java.time.Instant
import java.util.* import java.util.*
fun issueState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateRef { fun issueState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateRef {
val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), DUMMY_NOTARY) val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), notary)
tx.signWith(node.storage.myLegalIdentityKey) tx.signWith(node.storage.myLegalIdentityKey)
tx.signWith(DUMMY_NOTARY_KEY) tx.signWith(DUMMY_NOTARY_KEY)
val stx = tx.toSignedTransaction() val stx = tx.toSignedTransaction()
node.services.recordTransactions(listOf(stx)) node.services.recordTransactions(listOf(stx))
return StateRef(stx.id, 0) return StateRef(stx.id, 0)
} }
fun issueInvalidState(node: AbstractNode, notary: Party = DUMMY_NOTARY): StateRef {
val tx = DummyContract().generateInitial(node.info.identity.ref(0), Random().nextInt(), notary)
tx.setTime(Instant.now(), notary, 30.seconds)
tx.signWith(node.storage.myLegalIdentityKey)
val stx = tx.toSignedTransaction(false)
node.services.recordTransactions(listOf(stx))
return StateRef(stx.id, 0)
}

View File

@ -0,0 +1,35 @@
package com.r3corda.node.serialization
import com.r3corda.core.serialization.SerializeAsToken
import com.r3corda.core.serialization.SerializeAsTokenContext
import com.r3corda.core.serialization.SingletonSerializationToken
import java.time.Clock
import java.time.Instant
import java.time.ZoneId
import javax.annotation.concurrent.ThreadSafe
/**
* A [Clock] that tokenizes itself when serialized, and delegates to an underlying [Clock] implementation.
*/
@ThreadSafe
class NodeClock(private val delegateClock: Clock = Clock.systemUTC()) : Clock(), SerializeAsToken {
private val token = SingletonSerializationToken(this)
override fun toToken(context: SerializeAsTokenContext) = SingletonSerializationToken.registerWithContext(token, this, context)
override fun instant(): Instant {
return delegateClock.instant()
}
// Do not use this. Instead seek to use ZonedDateTime methods.
override fun withZone(zone: ZoneId): Clock {
throw UnsupportedOperationException("Tokenized clock does not support withZone()")
}
override fun getZone(): ZoneId {
return delegateClock.zone
}
}

View File

@ -1,8 +1,7 @@
package com.r3corda.node.services.api package com.r3corda.node.services.api
import com.r3corda.core.crypto.sha256
import com.r3corda.core.protocols.ProtocolStateMachine
import com.r3corda.core.serialization.SerializedBytes import com.r3corda.core.serialization.SerializedBytes
import com.r3corda.node.services.statemachine.ProtocolStateMachineImpl
/** /**
* Thread-safe storage of fiber checkpoints. * Thread-safe storage of fiber checkpoints.
@ -33,11 +32,8 @@ interface CheckpointStorage {
// This class will be serialised, so everything it points to transitively must also be serialisable (with Kryo). // This class will be serialised, so everything it points to transitively must also be serialisable (with Kryo).
data class Checkpoint( data class Checkpoint(
val serialisedFiber: SerializedBytes<out ProtocolStateMachine<*>>, val serialisedFiber: SerializedBytes<ProtocolStateMachineImpl<*>>,
val awaitingTopic: String, val awaitingTopic: String?,
val awaitingObjectOfType: String // java class name val awaitingPayloadType: String?,
) { val receivedPayload: Any?
override fun toString(): String { )
return "Checkpoint(#serialisedFiber=${serialisedFiber.sha256()}, awaitingTopic=$awaitingTopic, awaitingObjectOfType=$awaitingObjectOfType)"
}
}

View File

@ -1,10 +1,11 @@
package com.r3corda.node.services.api package com.r3corda.node.services.api
import com.codahale.metrics.MetricRegistry import com.codahale.metrics.MetricRegistry
import com.r3corda.core.serialization.SingletonSerializeAsToken
/** /**
* Provides access to various metrics and ways to notify monitoring services of things, for sysadmin purposes. * Provides access to various metrics and ways to notify monitoring services of things, for sysadmin purposes.
* This is not an interface because it is too lightweight to bother mocking out. * This is not an interface because it is too lightweight to bother mocking out.
*/ */
class MonitoringService(val metrics: MetricRegistry) class MonitoringService(val metrics: MetricRegistry) : SingletonSerializeAsToken()

View File

@ -2,6 +2,7 @@ package com.r3corda.node.services.identity
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.node.services.IdentityService import com.r3corda.core.node.services.IdentityService
import com.r3corda.core.serialization.SingletonSerializeAsToken
import java.security.PublicKey import java.security.PublicKey
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
@ -10,7 +11,7 @@ import javax.annotation.concurrent.ThreadSafe
* Simple identity service which caches parties and provides functionality for efficient lookup. * Simple identity service which caches parties and provides functionality for efficient lookup.
*/ */
@ThreadSafe @ThreadSafe
class InMemoryIdentityService() : IdentityService { class InMemoryIdentityService() : SingletonSerializeAsToken(), IdentityService {
private val keyToParties = ConcurrentHashMap<PublicKey, Party>() private val keyToParties = ConcurrentHashMap<PublicKey, Party>()
private val nameToParties = ConcurrentHashMap<String, Party>() private val nameToParties = ConcurrentHashMap<String, Party>()

View File

@ -3,6 +3,7 @@ package com.r3corda.node.services.keys
import com.r3corda.core.ThreadBox import com.r3corda.core.ThreadBox
import com.r3corda.core.crypto.generateKeyPair import com.r3corda.core.crypto.generateKeyPair
import com.r3corda.core.node.services.KeyManagementService import com.r3corda.core.node.services.KeyManagementService
import com.r3corda.core.serialization.SingletonSerializeAsToken
import java.security.KeyPair import java.security.KeyPair
import java.security.PrivateKey import java.security.PrivateKey
import java.security.PublicKey import java.security.PublicKey
@ -21,7 +22,7 @@ import javax.annotation.concurrent.ThreadSafe
* etc * etc
*/ */
@ThreadSafe @ThreadSafe
class E2ETestKeyManagementService : KeyManagementService { class E2ETestKeyManagementService() : SingletonSerializeAsToken(), KeyManagementService {
private class InnerState { private class InnerState {
val keys = HashMap<PublicKey, PrivateKey>() val keys = HashMap<PublicKey, PrivateKey>()
} }

View File

@ -4,8 +4,9 @@ import com.google.common.net.HostAndPort
import com.r3corda.core.RunOnCallerThread import com.r3corda.core.RunOnCallerThread
import com.r3corda.core.ThreadBox import com.r3corda.core.ThreadBox
import com.r3corda.core.messaging.* import com.r3corda.core.messaging.*
import com.r3corda.node.internal.Node import com.r3corda.core.serialization.SingletonSerializeAsToken
import com.r3corda.core.utilities.loggerFor import com.r3corda.core.utilities.loggerFor
import com.r3corda.node.internal.Node
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.TransportConfiguration import org.apache.activemq.artemis.api.core.TransportConfiguration
import org.apache.activemq.artemis.api.core.client.* import org.apache.activemq.artemis.api.core.client.*
@ -52,7 +53,7 @@ import javax.annotation.concurrent.ThreadSafe
*/ */
@ThreadSafe @ThreadSafe
class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort, class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort,
val defaultExecutor: Executor = RunOnCallerThread) : MessagingService { val defaultExecutor: Executor = RunOnCallerThread) : SingletonSerializeAsToken(), MessagingService {
// In future: can contain onion routing info, etc. // In future: can contain onion routing info, etc.
private data class Address(val hostAndPort: HostAndPort) : SingleMessageRecipient private data class Address(val hostAndPort: HostAndPort) : SingleMessageRecipient
@ -124,9 +125,8 @@ class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort,
val secManager = ActiveMQJAASSecurityManager(InVMLoginModule::class.java.name, secConfig) val secManager = ActiveMQJAASSecurityManager(InVMLoginModule::class.java.name, secConfig)
mq.setSecurityManager(secManager) mq.setSecurityManager(secManager)
// Currently we cannot find out if something goes wrong during startup :( This is bug ARTEMIS-388 filed by me. // TODO Currently we cannot find out if something goes wrong during startup :( This is bug ARTEMIS-388 filed by me.
// The fix should be in the 1.3.0 release: // The fix should be in the 1.3.0 release:
//
// https://issues.apache.org/jira/browse/ARTEMIS-388 // https://issues.apache.org/jira/browse/ARTEMIS-388
mq.start() mq.start()
@ -136,12 +136,13 @@ class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort,
// Create a queue on which to receive messages and set up the handler. // Create a queue on which to receive messages and set up the handler.
session = clientFactory.createSession() session = clientFactory.createSession()
session.createQueue(myHostPort.toString(), "inbound", false) session.createQueue(myHostPort.toString(), "inbound", false)
inboundConsumer = session.createConsumer("inbound").setMessageHandler { message: ClientMessage -> inboundConsumer = session.createConsumer("inbound").setMessageHandler { message: ClientMessage ->
// This code runs for every inbound message. // This code runs for every inbound message.
try { try {
if (!message.containsProperty(TOPIC_PROPERTY)) { if (!message.containsProperty(TOPIC_PROPERTY)) {
log.warn("Received message without a ${TOPIC_PROPERTY} property, ignoring") log.warn("Received message without a $TOPIC_PROPERTY property, ignoring")
return@setMessageHandler return@setMessageHandler
} }
val topic = message.getStringProperty(TOPIC_PROPERTY) val topic = message.getStringProperty(TOPIC_PROPERTY)
@ -159,6 +160,8 @@ class ArtemisMessagingService(val directory: Path, val myHostPort: HostAndPort,
deliverMessage(msg) deliverMessage(msg)
} finally { } finally {
// TODO the message is delivered onto an executor and so we may be acking the message before we've
// finished processing it
message.acknowledge() message.acknowledge()
} }
} }

View File

@ -6,6 +6,7 @@ import com.google.common.util.concurrent.MoreExecutors
import com.r3corda.core.ThreadBox import com.r3corda.core.ThreadBox
import com.r3corda.core.crypto.sha256 import com.r3corda.core.crypto.sha256
import com.r3corda.core.messaging.* import com.r3corda.core.messaging.*
import com.r3corda.core.serialization.SingletonSerializeAsToken
import com.r3corda.core.utilities.loggerFor import com.r3corda.core.utilities.loggerFor
import com.r3corda.core.utilities.trace import com.r3corda.core.utilities.trace
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@ -28,7 +29,7 @@ import kotlin.concurrent.thread
* testing). * testing).
*/ */
@ThreadSafe @ThreadSafe
class InMemoryMessagingNetwork { class InMemoryMessagingNetwork() : SingletonSerializeAsToken() {
companion object { companion object {
val MESSAGES_LOG_NAME = "messages" val MESSAGES_LOG_NAME = "messages"
private val log = LoggerFactory.getLogger(MESSAGES_LOG_NAME) private val log = LoggerFactory.getLogger(MESSAGES_LOG_NAME)
@ -167,7 +168,7 @@ class InMemoryMessagingNetwork {
* An instance can be obtained by creating a builder and then using the start method. * An instance can be obtained by creating a builder and then using the start method.
*/ */
@ThreadSafe @ThreadSafe
inner class InMemoryMessaging(private val manuallyPumped: Boolean, private val handle: Handle) : MessagingService { inner class InMemoryMessaging(private val manuallyPumped: Boolean, private val handle: Handle) : SingletonSerializeAsToken(), MessagingService {
inner class Handler(val executor: Executor?, val topic: String, inner class Handler(val executor: Executor?, val topic: String,
val callback: (Message, MessageHandlerRegistration) -> Unit) : MessageHandlerRegistration val callback: (Message, MessageHandlerRegistration) -> Unit) : MessageHandlerRegistration

View File

@ -15,6 +15,7 @@ import com.r3corda.core.node.services.NetworkMapCache
import com.r3corda.core.node.services.ServiceType import com.r3corda.core.node.services.ServiceType
import com.r3corda.core.node.services.TOPIC_DEFAULT_POSTFIX import com.r3corda.core.node.services.TOPIC_DEFAULT_POSTFIX
import com.r3corda.core.random63BitValue import com.r3corda.core.random63BitValue
import com.r3corda.core.serialization.SingletonSerializeAsToken
import com.r3corda.core.serialization.deserialize import com.r3corda.core.serialization.deserialize
import com.r3corda.core.serialization.serialize import com.r3corda.core.serialization.serialize
import com.r3corda.node.services.api.RegulatorService import com.r3corda.node.services.api.RegulatorService
@ -30,7 +31,7 @@ import javax.annotation.concurrent.ThreadSafe
* Extremely simple in-memory cache of the network map. * Extremely simple in-memory cache of the network map.
*/ */
@ThreadSafe @ThreadSafe
open class InMemoryNetworkMapCache() : NetworkMapCache { open class InMemoryNetworkMapCache() : SingletonSerializeAsToken(), NetworkMapCache {
override val networkMapNodes: List<NodeInfo> override val networkMapNodes: List<NodeInfo>
get() = get(NetworkMapService.Type) get() = get(NetworkMapService.Type)
override val regulators: List<NodeInfo> override val regulators: List<NodeInfo>
@ -46,7 +47,7 @@ open class InMemoryNetworkMapCache() : NetworkMapCache {
protected var registeredNodes = Collections.synchronizedMap(HashMap<Party, NodeInfo>()) protected var registeredNodes = Collections.synchronizedMap(HashMap<Party, NodeInfo>())
override fun get() = registeredNodes.map { it.value } override fun get() = registeredNodes.map { it.value }
override fun get(serviceType: ServiceType) = registeredNodes.filterValues { it.advertisedServices.contains(serviceType) }.map { it.value } override fun get(serviceType: ServiceType) = registeredNodes.filterValues { it.advertisedServices.any { it.isSubTypeOf(serviceType) } }.map { it.value }
override fun getRecommended(type: ServiceType, contract: Contract, vararg party: Party): NodeInfo? = get(type).firstOrNull() override fun getRecommended(type: ServiceType, contract: Contract, vararg party: Party): NodeInfo? = get(type).firstOrNull()
override fun getNodeByLegalName(name: String) = get().singleOrNull { it.identity.name == name } override fun getNodeByLegalName(name: String) = get().singleOrNull { it.identity.name == name }
override fun getNodeByPublicKey(publicKey: PublicKey) = get().singleOrNull { it.identity.owningKey == publicKey } override fun getNodeByPublicKey(publicKey: PublicKey) = get().singleOrNull { it.identity.owningKey == publicKey }

View File

@ -1,15 +1,8 @@
/*
* Copyright 2016 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
* set forth therein.
*
* All other rights reserved.
*/
package com.r3corda.node.services.network package com.r3corda.node.services.network
import co.paralleluniverse.common.util.VisibleForTesting import co.paralleluniverse.common.util.VisibleForTesting
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.DummyPublicKey import com.r3corda.core.crypto.DummyPublicKey
import com.r3corda.core.crypto.Party
import com.r3corda.core.messaging.SingleMessageRecipient import com.r3corda.core.messaging.SingleMessageRecipient
import com.r3corda.core.node.NodeInfo import com.r3corda.core.node.NodeInfo

View File

@ -1,10 +1,11 @@
package com.r3corda.node.services.persistence package com.r3corda.node.services.persistence
import com.r3corda.core.crypto.Party
import com.r3corda.core.contracts.SignedTransaction import com.r3corda.core.contracts.SignedTransaction
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.services.AttachmentStorage import com.r3corda.core.node.services.AttachmentStorage
import com.r3corda.core.node.services.StorageService import com.r3corda.core.node.services.StorageService
import com.r3corda.core.serialization.SingletonSerializeAsToken
import com.r3corda.core.utilities.RecordingMap import com.r3corda.core.utilities.RecordingMap
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.security.KeyPair import java.security.KeyPair
@ -15,7 +16,7 @@ open class StorageServiceImpl(override val attachments: AttachmentStorage,
override val myLegalIdentity: Party = Party("Unit test party", myLegalIdentityKey.public), override val myLegalIdentity: Party = Party("Unit test party", myLegalIdentityKey.public),
// This parameter is for unit tests that want to observe operation details. // This parameter is for unit tests that want to observe operation details.
val recordingAs: (String) -> String = { tableName -> "" }) val recordingAs: (String) -> String = { tableName -> "" })
: StorageService { : SingletonSerializeAsToken(), StorageService {
protected val tables = HashMap<String, MutableMap<*, *>>() protected val tables = HashMap<String, MutableMap<*, *>>()
private fun <K, V> getMapOriginal(tableName: String): MutableMap<K, V> { private fun <K, V> getMapOriginal(tableName: String): MutableMap<K, V> {

View File

@ -3,17 +3,11 @@ package com.r3corda.node.services.statemachine
import co.paralleluniverse.fibers.Fiber import co.paralleluniverse.fibers.Fiber
import co.paralleluniverse.fibers.FiberScheduler import co.paralleluniverse.fibers.FiberScheduler
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import co.paralleluniverse.io.serialization.kryo.KryoSerializer
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import com.r3corda.core.messaging.MessageRecipients import com.r3corda.core.messaging.MessageRecipients
import com.r3corda.node.services.statemachine.StateMachineManager
import com.r3corda.core.node.ServiceHub
import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.protocols.ProtocolStateMachine import com.r3corda.core.protocols.ProtocolStateMachine
import com.r3corda.core.serialization.SerializedBytes
import com.r3corda.core.serialization.createKryo
import com.r3corda.core.serialization.serialize
import com.r3corda.core.utilities.UntrustworthyData import com.r3corda.core.utilities.UntrustworthyData
import com.r3corda.node.services.api.ServiceHubInternal import com.r3corda.node.services.api.ServiceHubInternal
import org.slf4j.Logger import org.slf4j.Logger
@ -27,11 +21,11 @@ import org.slf4j.LoggerFactory
* a protocol invokes a sub-protocol, then it will pass along the PSM to the child. The call method of the topmost * a protocol invokes a sub-protocol, then it will pass along the PSM to the child. The call method of the topmost
* logic element gets to return the value that the entire state machine resolves to. * logic element gets to return the value that the entire state machine resolves to.
*/ */
class ProtocolStateMachineImpl<R>(val logic: ProtocolLogic<R>, scheduler: FiberScheduler, val loggerName: String) : Fiber<R>("protocol", scheduler), ProtocolStateMachine<R> { class ProtocolStateMachineImpl<R>(val logic: ProtocolLogic<R>, scheduler: FiberScheduler, private val loggerName: String) : Fiber<R>("protocol", scheduler), ProtocolStateMachine<R> {
// These fields shouldn't be serialised, so they are marked @Transient. // These fields shouldn't be serialised, so they are marked @Transient.
@Transient private var suspendAction: ((result: StateMachineManager.FiberRequest, serialisedFiber: SerializedBytes<ProtocolStateMachineImpl<*>>) -> Unit)? = null @Transient private var suspendAction: ((result: StateMachineManager.FiberRequest, fiber: ProtocolStateMachineImpl<*>) -> Unit)? = null
@Transient private var resumeWithObject: Any? = null @Transient private var receivedPayload: Any? = null
@Transient lateinit override var serviceHub: ServiceHubInternal @Transient lateinit override var serviceHub: ServiceHubInternal
@Transient private var _logger: Logger? = null @Transient private var _logger: Logger? = null
@ -58,11 +52,11 @@ class ProtocolStateMachineImpl<R>(val logic: ProtocolLogic<R>, scheduler: FiberS
} }
fun prepareForResumeWith(serviceHub: ServiceHubInternal, fun prepareForResumeWith(serviceHub: ServiceHubInternal,
withObject: Any?, receivedPayload: Any?,
suspendAction: (StateMachineManager.FiberRequest, SerializedBytes<ProtocolStateMachineImpl<*>>) -> Unit) { suspendAction: (StateMachineManager.FiberRequest, ProtocolStateMachineImpl<*>) -> Unit) {
this.suspendAction = suspendAction
this.resumeWithObject = withObject
this.serviceHub = serviceHub this.serviceHub = serviceHub
this.receivedPayload = receivedPayload
this.suspendAction = suspendAction
} }
@Suspendable @Suppress("UNCHECKED_CAST") @Suspendable @Suppress("UNCHECKED_CAST")
@ -81,9 +75,10 @@ class ProtocolStateMachineImpl<R>(val logic: ProtocolLogic<R>, scheduler: FiberS
@Suspendable @Suppress("UNCHECKED_CAST") @Suspendable @Suppress("UNCHECKED_CAST")
private fun <T : Any> suspendAndExpectReceive(with: StateMachineManager.FiberRequest): UntrustworthyData<T> { private fun <T : Any> suspendAndExpectReceive(with: StateMachineManager.FiberRequest): UntrustworthyData<T> {
suspend(with) suspend(with)
val tmp = resumeWithObject ?: throw IllegalStateException("Expected to receive something") check(receivedPayload != null) { "Expected to receive something" }
resumeWithObject = null val untrustworthy = UntrustworthyData(receivedPayload as T)
return UntrustworthyData(tmp as T) receivedPayload = null
return untrustworthy
} }
@Suspendable @Suppress("UNCHECKED_CAST") @Suspendable @Suppress("UNCHECKED_CAST")
@ -108,10 +103,13 @@ class ProtocolStateMachineImpl<R>(val logic: ProtocolLogic<R>, scheduler: FiberS
@Suspendable @Suspendable
private fun suspend(with: StateMachineManager.FiberRequest) { private fun suspend(with: StateMachineManager.FiberRequest) {
parkAndSerialize { fiber, serializer -> parkAndSerialize { fiber, serializer ->
// We don't use the passed-in serializer here, because we need to use our own augmented Kryo. try {
val deserializer = getFiberSerializer(false) as KryoSerializer suspendAction!!(with, this)
val kryo = createKryo(deserializer.kryo) } catch (t: Throwable) {
suspendAction!!(with, this.serialize(kryo)) logger.warn("Captured exception which was swallowed by Quasar", t)
// TODO to throw or not to throw, that is the question
throw t
}
} }
} }

View File

@ -4,18 +4,15 @@ import co.paralleluniverse.fibers.Fiber
import co.paralleluniverse.fibers.FiberExecutorScheduler import co.paralleluniverse.fibers.FiberExecutorScheduler
import co.paralleluniverse.io.serialization.kryo.KryoSerializer import co.paralleluniverse.io.serialization.kryo.KryoSerializer
import com.codahale.metrics.Gauge import com.codahale.metrics.Gauge
import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.Kryo
import com.google.common.base.Throwables import com.google.common.base.Throwables
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.r3corda.core.abbreviate
import com.r3corda.core.messaging.MessageRecipients import com.r3corda.core.messaging.MessageRecipients
import com.r3corda.core.messaging.runOnNextMessage import com.r3corda.core.messaging.runOnNextMessage
import com.r3corda.core.messaging.send import com.r3corda.core.messaging.send
import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.protocols.ProtocolStateMachine import com.r3corda.core.serialization.*
import com.r3corda.core.serialization.SerializedBytes
import com.r3corda.core.serialization.THREAD_LOCAL_KRYO
import com.r3corda.core.serialization.createKryo
import com.r3corda.core.serialization.deserialize
import com.r3corda.core.then import com.r3corda.core.then
import com.r3corda.core.utilities.ProgressTracker import com.r3corda.core.utilities.ProgressTracker
import com.r3corda.core.utilities.trace import com.r3corda.core.utilities.trace
@ -27,7 +24,6 @@ import java.io.PrintWriter
import java.io.StringWriter import java.io.StringWriter
import java.util.* import java.util.*
import java.util.Collections.synchronizedMap import java.util.Collections.synchronizedMap
import java.util.concurrent.atomic.AtomicBoolean
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
/** /**
@ -51,12 +47,10 @@ import javax.annotation.concurrent.ThreadSafe
* TODO: Timeouts * TODO: Timeouts
* TODO: Surfacing of exceptions via an API and/or management UI * TODO: Surfacing of exceptions via an API and/or management UI
* TODO: Ability to control checkpointing explicitly, for cases where you know replaying a message can't hurt * TODO: Ability to control checkpointing explicitly, for cases where you know replaying a message can't hurt
* TODO: Make Kryo (de)serialize markers for heavy objects that are currently in the service hub. This avoids mistakes
* where services are temporarily put on the stack.
* TODO: Implement stub/skel classes that provide a basic RPC framework on top of this. * TODO: Implement stub/skel classes that provide a basic RPC framework on top of this.
*/ */
@ThreadSafe @ThreadSafe
class StateMachineManager(val serviceHub: ServiceHubInternal, val checkpointStorage: CheckpointStorage, val executor: AffinityExecutor) { class StateMachineManager(val serviceHub: ServiceHubInternal, tokenizableServices: List<Any>, val checkpointStorage: CheckpointStorage, val executor: AffinityExecutor) {
inner class FiberScheduler : FiberExecutorScheduler("Same thread scheduler", executor) inner class FiberScheduler : FiberExecutorScheduler("Same thread scheduler", executor)
val scheduler = FiberScheduler() val scheduler = FiberScheduler()
@ -76,13 +70,16 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, val checkpointStor
private val totalStartedProtocols = metrics.counter("Protocols.Started") private val totalStartedProtocols = metrics.counter("Protocols.Started")
private val totalFinishedProtocols = metrics.counter("Protocols.Finished") private val totalFinishedProtocols = metrics.counter("Protocols.Finished")
// Context for tokenized services in checkpoints
private val serializationContext = SerializeAsTokenContext(tokenizableServices, quasarKryo())
/** Returns a list of all state machines executing the given protocol logic at the top level (subprotocols do not count) */ /** Returns a list of all state machines executing the given protocol logic at the top level (subprotocols do not count) */
fun <T> findStateMachines(klass: Class<out ProtocolLogic<T>>): List<Pair<ProtocolLogic<T>, ListenableFuture<T>>> { fun <P : ProtocolLogic<T>, T> findStateMachines(protocolClass: Class<P>): List<Pair<P, ListenableFuture<T>>> {
synchronized(stateMachines) { synchronized(stateMachines) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return stateMachines.keys return stateMachines.keys
.map { it.logic } .map { it.logic }
.filterIsInstance(klass) .filterIsInstance(protocolClass)
.map { it to (it.psm as ProtocolStateMachineImpl<T>).resultFuture } .map { it to (it.psm as ProtocolStateMachineImpl<T>).resultFuture }
} }
} }
@ -94,57 +91,60 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, val checkpointStor
field.get(null) field.get(null)
} }
companion object {
var restoreCheckpointsOnStart = true
}
init { init {
Fiber.setDefaultUncaughtExceptionHandler { fiber, throwable -> Fiber.setDefaultUncaughtExceptionHandler { fiber, throwable ->
(fiber as ProtocolStateMachineImpl<*>).logger.error("Caught exception from protocol", throwable) (fiber as ProtocolStateMachineImpl<*>).logger.error("Caught exception from protocol", throwable)
} }
if (restoreCheckpointsOnStart)
restoreCheckpoints()
} }
/** Reads the database map and resurrects any serialised state machines. */ fun start() {
private fun restoreCheckpoints() { checkpointStorage.checkpoints.forEach { restoreCheckpoint(it) }
for (checkpoint in checkpointStorage.checkpoints) { }
// Grab the Kryo engine configured by Quasar for its own stuff, and then do our own configuration on top
// so we can deserialised the nested stream that holds the fiber. private fun restoreCheckpoint(checkpoint: Checkpoint) {
val psm = deserializeFiber(checkpoint.serialisedFiber) val fiber = deserializeFiber(checkpoint.serialisedFiber)
initFiber(psm, checkpoint) initFiber(fiber, checkpoint)
val awaitingObjectOfType = Class.forName(checkpoint.awaitingObjectOfType)
val topic = checkpoint.awaitingTopic val topic = checkpoint.awaitingTopic
if (topic != null) {
psm.logger.info("restored ${psm.logic} - was previously awaiting on topic $topic") val awaitingPayloadType = Class.forName(checkpoint.awaitingPayloadType)
fiber.logger.info("Restored ${fiber.logic} - it was previously waiting for message of type ${awaitingPayloadType.name} on topic $topic")
// And now re-wire the deserialised continuation back up to the network service. iterateOnResponse(fiber, awaitingPayloadType, checkpoint.serialisedFiber, topic) {
serviceHub.networkService.runOnNextMessage(topic, executor) { netMsg ->
// TODO: See security note below.
val obj: Any = THREAD_LOCAL_KRYO.get().readClassAndObject(Input(netMsg.data))
if (!awaitingObjectOfType.isInstance(obj))
throw ClassCastException("Received message of unexpected type: ${obj.javaClass.name} vs ${awaitingObjectOfType.name}")
psm.logger.trace { "<- $topic : message of type ${obj.javaClass.name}" }
iterateStateMachine(psm, obj) {
try { try {
Fiber.unparkDeserialized(it, scheduler) Fiber.unparkDeserialized(fiber, scheduler)
} catch (e: Throwable) { } catch (e: Throwable) {
logError(e, obj, topic, it) logError(e, it, topic, fiber)
}
}
} else {
fiber.logger.info("Restored ${fiber.logic} - it was not waiting on any message; received payload: ${checkpoint.receivedPayload.toString().abbreviate(50)}")
executor.executeASAP {
iterateStateMachine(fiber, checkpoint.receivedPayload) {
try {
Fiber.unparkDeserialized(fiber, scheduler)
} catch (e: Throwable) {
logError(e, it, null, fiber)
} }
} }
} }
} }
} }
private fun deserializeFiber(serialisedFiber: SerializedBytes<out ProtocolStateMachine<*>>): ProtocolStateMachineImpl<*> { private fun deserializeFiber(serialisedFiber: SerializedBytes<ProtocolStateMachineImpl<*>>): ProtocolStateMachineImpl<*> {
val deserializer = Fiber.getFiberSerializer(false) as KryoSerializer val kryo = quasarKryo()
val kryo = createKryo(deserializer.kryo) // put the map of token -> tokenized into the kryo context
return serialisedFiber.deserialize(kryo) as ProtocolStateMachineImpl<*> SerializeAsTokenSerializer.setContext(kryo, serializationContext)
return serialisedFiber.deserialize(kryo)
} }
private fun logError(e: Throwable, obj: Any, topic: String, psm: ProtocolStateMachineImpl<*>) { private fun quasarKryo(): Kryo {
val serializer = Fiber.getFiberSerializer(false) as KryoSerializer
return createKryo(serializer.kryo)
}
private fun logError(e: Throwable, payload: Any?, topic: String?, psm: ProtocolStateMachineImpl<*>) {
psm.logger.error("Protocol state machine ${psm.javaClass.name} threw '${Throwables.getRootCause(e)}' " + psm.logger.error("Protocol state machine ${psm.javaClass.name} threw '${Throwables.getRootCause(e)}' " +
"when handling a message of type ${obj.javaClass.name} on topic $topic") "when handling a message of type ${payload?.javaClass?.name} on topic $topic")
if (psm.logger.isTraceEnabled) { if (psm.logger.isTraceEnabled) {
val s = StringWriter() val s = StringWriter()
Throwables.getRootCause(e).printStackTrace(PrintWriter(s)) Throwables.getRootCause(e).printStackTrace(PrintWriter(s))
@ -152,11 +152,11 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, val checkpointStor
} }
} }
private fun initFiber(psm: ProtocolStateMachineImpl<*>, checkpoint: Checkpoint?) { private fun initFiber(fiber: ProtocolStateMachineImpl<*>, checkpoint: Checkpoint?) {
stateMachines[psm] = checkpoint stateMachines[fiber] = checkpoint
psm.resultFuture.then(executor) { fiber.resultFuture.then(executor) {
psm.logic.progressTracker?.currentStep = ProgressTracker.DONE fiber.logic.progressTracker?.currentStep = ProgressTracker.DONE
val finalCheckpoint = stateMachines.remove(psm) val finalCheckpoint = stateMachines.remove(fiber)
if (finalCheckpoint != null) { if (finalCheckpoint != null) {
checkpointStorage.removeCheckpoint(finalCheckpoint) checkpointStorage.removeCheckpoint(finalCheckpoint)
} }
@ -176,7 +176,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, val checkpointStor
initFiber(fiber, null) initFiber(fiber, null)
executor.executeASAP { executor.executeASAP {
iterateStateMachine(fiber, null) { iterateStateMachine(fiber, null) {
it.start() fiber.start()
} }
totalStartedProtocols.inc() totalStartedProtocols.inc()
} }
@ -187,9 +187,12 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, val checkpointStor
} }
} }
private fun replaceCheckpoint(psm: ProtocolStateMachineImpl<*>, newCheckpoint: Checkpoint) { private fun updateCheckpoint(psm: ProtocolStateMachineImpl<*>,
// It's OK for this to be unsynchronised, as the prev/new byte arrays are specific to a continuation instance, serialisedFiber: SerializedBytes<ProtocolStateMachineImpl<*>>,
// and the underlying map provided by the database layer is expected to be thread safe. awaitingTopic: String?,
awaitingPayloadType: Class<*>?,
receivedPayload: Any?) {
val newCheckpoint = Checkpoint(serialisedFiber, awaitingTopic, awaitingPayloadType?.name, receivedPayload)
val previousCheckpoint = stateMachines.put(psm, newCheckpoint) val previousCheckpoint = stateMachines.put(psm, newCheckpoint)
if (previousCheckpoint != null) { if (previousCheckpoint != null) {
checkpointStorage.removeCheckpoint(previousCheckpoint) checkpointStorage.removeCheckpoint(previousCheckpoint)
@ -199,75 +202,93 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, val checkpointStor
} }
private fun iterateStateMachine(psm: ProtocolStateMachineImpl<*>, private fun iterateStateMachine(psm: ProtocolStateMachineImpl<*>,
obj: Any?, receivedPayload: Any?,
resumeFunc: (ProtocolStateMachineImpl<*>) -> Unit) { resumeAction: (Any?) -> Unit) {
executor.checkOnThread() executor.checkOnThread()
val onSuspend = fun(request: FiberRequest, serialisedFiber: SerializedBytes<ProtocolStateMachineImpl<*>>) { psm.prepareForResumeWith(serviceHub, receivedPayload) { request, serialisedFiber ->
psm.logger.trace { "Suspended fiber ${psm.id} ${psm.logic}" }
onNextSuspend(psm, request, serialisedFiber)
}
psm.logger.trace { "Waking up fiber ${psm.id} ${psm.logic}" }
resumeAction(receivedPayload)
}
private fun onNextSuspend(psm: ProtocolStateMachineImpl<*>,
request: FiberRequest,
fiber: ProtocolStateMachineImpl<*>) {
// We have a request to do something: send, receive, or send-and-receive. // We have a request to do something: send, receive, or send-and-receive.
if (request is FiberRequest.ExpectingResponse<*>) { if (request is FiberRequest.ExpectingResponse<*>) {
// Prepare a listener on the network that runs in the background thread when we received a message. // We don't use the passed-in serializer here, because we need to use our own augmented Kryo.
checkpointAndSetupMessageHandler(psm, request, serialisedFiber) val kryo = quasarKryo()
// add the map of tokens -> tokenizedServices to the kyro context
SerializeAsTokenSerializer.setContext(kryo, serializationContext)
val serialisedFiber = fiber.serialize(kryo)
// Prepare a listener on the network that runs in the background thread when we receive a message.
checkpointOnExpectingResponse(psm, request, serialisedFiber)
} }
// If an object to send was provided (not null), send it now. // If a non-null payload to send was provided, send it now.
request.obj?.let { request.payload?.let {
val topic = "${request.topic}.${request.sessionIDForSend}" val topic = "${request.topic}.${request.sessionIDForSend}"
psm.logger.trace { "-> ${request.destination}/$topic : message of type ${it.javaClass.name}" } psm.logger.trace { "Sending message of type ${it.javaClass.name} using topic $topic to ${request.destination} (${it.toString().abbreviate(50)})" }
serviceHub.networkService.send(topic, it, request.destination!!) serviceHub.networkService.send(topic, it, request.destination!!)
} }
if (request is FiberRequest.NotExpectingResponse) { if (request is FiberRequest.NotExpectingResponse) {
// We sent a message, but don't expect a response, so re-enter the continuation to let it keep going. // We sent a message, but don't expect a response, so re-enter the continuation to let it keep going.
iterateStateMachine(psm, null) { iterateStateMachine(psm, null) {
try { try {
Fiber.unpark(it, QUASAR_UNBLOCKER) Fiber.unpark(psm, QUASAR_UNBLOCKER)
} catch(e: Throwable) { } catch(e: Throwable) {
logError(e, request.obj!!, request.topic, it) logError(e, request.payload, request.topic, psm)
} }
} }
} }
} }
psm.prepareForResumeWith(serviceHub, obj, onSuspend) private fun checkpointOnExpectingResponse(psm: ProtocolStateMachineImpl<*>,
resumeFunc(psm)
}
private fun checkpointAndSetupMessageHandler(psm: ProtocolStateMachineImpl<*>,
request: FiberRequest.ExpectingResponse<*>, request: FiberRequest.ExpectingResponse<*>,
serialisedFiber: SerializedBytes<ProtocolStateMachineImpl<*>>) { serialisedFiber: SerializedBytes<ProtocolStateMachineImpl<*>>) {
executor.checkOnThread() executor.checkOnThread()
val topic = "${request.topic}.${request.sessionIDForReceive}" val topic = "${request.topic}.${request.sessionIDForReceive}"
val newCheckpoint = Checkpoint(serialisedFiber, topic, request.responseType.name) updateCheckpoint(psm, serialisedFiber, topic, request.responseType, null)
replaceCheckpoint(psm, newCheckpoint) psm.logger.trace { "Preparing to receive message of type ${request.responseType.name} on topic $topic" }
psm.logger.trace { "Waiting for message of type ${request.responseType.name} on $topic" } iterateOnResponse(psm, request.responseType, serialisedFiber, topic) {
val consumed = AtomicBoolean() try {
Fiber.unpark(psm, QUASAR_UNBLOCKER)
} catch(e: Throwable) {
logError(e, it, topic, psm)
}
}
}
private fun iterateOnResponse(psm: ProtocolStateMachineImpl<*>,
responseType: Class<*>,
serialisedFiber: SerializedBytes<ProtocolStateMachineImpl<*>>,
topic: String,
resumeAction: (Any?) -> Unit) {
serviceHub.networkService.runOnNextMessage(topic, executor) { netMsg -> serviceHub.networkService.runOnNextMessage(topic, executor) { netMsg ->
// Some assertions to ensure we don't execute on the wrong thread or get executed more than once. // Assertion to ensure we don't execute on the wrong thread.
executor.checkOnThread() executor.checkOnThread()
check(netMsg.topic == topic) { "Topic mismatch: ${netMsg.topic} vs $topic" }
check(!consumed.getAndSet(true))
// TODO: This is insecure: we should not deserialise whatever we find and *then* check. // TODO: This is insecure: we should not deserialise whatever we find and *then* check.
//
// We should instead verify as we read the data that it's what we are expecting and throw as early as // We should instead verify as we read the data that it's what we are expecting and throw as early as
// possible. We only do it this way for convenience during the prototyping stage. Note that this means // possible. We only do it this way for convenience during the prototyping stage. Note that this means
// we could simply not require the programmer to specify the expected return type at all, and catch it // we could simply not require the programmer to specify the expected return type at all, and catch it
// at the last moment when we do the downcast. However this would make protocol code harder to read and // at the last moment when we do the downcast. However this would make protocol code harder to read and
// make it more difficult to migrate to a more explicit serialisation scheme later. // make it more difficult to migrate to a more explicit serialisation scheme later.
val obj: Any = THREAD_LOCAL_KRYO.get().readClassAndObject(Input(netMsg.data)) val payload = netMsg.data.deserialize<Any>()
if (!request.responseType.isInstance(obj)) check(responseType.isInstance(payload)) { "Expected message of type ${responseType.name} but got ${payload.javaClass.name}" }
throw IllegalStateException("Expected message of type ${request.responseType.name} but got ${obj.javaClass.name}", request.stackTraceInCaseOfProblems) // Update the fiber's checkpoint so that it's no longer waiting on a response, but rather has the received payload
iterateStateMachine(psm, obj) { updateCheckpoint(psm, serialisedFiber, null, null, payload)
try { psm.logger.trace { "Received message of type ${payload.javaClass.name} on topic $topic (${payload.toString().abbreviate(50)})" }
Fiber.unpark(it, QUASAR_UNBLOCKER) iterateStateMachine(psm, payload, resumeAction)
} catch(e: Throwable) {
logError(e, obj, topic, it)
}
}
} }
} }
// TODO: Clean this up // TODO: Clean this up
open class FiberRequest(val topic: String, val destination: MessageRecipients?, open class FiberRequest(val topic: String,
val sessionIDForSend: Long, val sessionIDForReceive: Long, val obj: Any?) { val destination: MessageRecipients?,
val sessionIDForSend: Long,
val sessionIDForReceive: Long,
val payload: Any?) {
// This is used to identify where we suspended, in case of message mismatch errors and other things where we // This is used to identify where we suspended, in case of message mismatch errors and other things where we
// don't have the original stack trace because it's in a suspended fiber. // don't have the original stack trace because it's in a suspended fiber.
val stackTraceInCaseOfProblems = StackSnapshot() val stackTraceInCaseOfProblems = StackSnapshot()

View File

@ -1,100 +1,46 @@
package com.r3corda.node.services.transactions package com.r3corda.node.services.transactions
import com.r3corda.core.contracts.TimestampCommand
import com.r3corda.core.contracts.WireTransaction
import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SignedData
import com.r3corda.core.crypto.signWithECDSA
import com.r3corda.core.messaging.MessagingService import com.r3corda.core.messaging.MessagingService
import com.r3corda.core.messaging.SingleMessageRecipient
import com.r3corda.core.node.services.ServiceType import com.r3corda.core.node.services.ServiceType
import com.r3corda.core.node.services.UniquenessException import com.r3corda.core.node.services.TimestampChecker
import com.r3corda.core.node.services.UniquenessProvider import com.r3corda.core.node.services.UniquenessProvider
import com.r3corda.core.noneOrSingle
import com.r3corda.core.serialization.SerializedBytes
import com.r3corda.core.serialization.deserialize
import com.r3corda.core.serialization.serialize
import com.r3corda.core.utilities.loggerFor
import com.r3corda.node.services.api.AbstractNodeService import com.r3corda.node.services.api.AbstractNodeService
import com.r3corda.protocols.NotaryError import com.r3corda.node.services.statemachine.StateMachineManager
import com.r3corda.protocols.NotaryException
import com.r3corda.protocols.NotaryProtocol import com.r3corda.protocols.NotaryProtocol
import java.security.KeyPair
/** /**
* A Notary service acts as the final signer of a transaction ensuring two things: * A Notary service acts as the final signer of a transaction ensuring two things:
* - The (optional) timestamp of the transaction is valid * - The (optional) timestamp of the transaction is valid
* - None of the referenced input states have previously been consumed by a transaction signed by this Notary * - None of the referenced input states have previously been consumed by a transaction signed by this Notary
* *
* A transaction has to be signed by a Notary to be considered valid (except for output-only transactions w/o a timestamp) * A transaction has to be signed by a Notary to be considered valid (except for output-only transactions without a timestamp).
*
* This is the base implementation that can be customised with specific Notary transaction commit protocol
*/ */
class NotaryService(net: MessagingService, abstract class NotaryService(val smm: StateMachineManager,
val identity: Party, net: MessagingService,
val signingKey: KeyPair, val timestampChecker: TimestampChecker,
val uniquenessProvider: UniquenessProvider, val uniquenessProvider: UniquenessProvider) : AbstractNodeService(net) {
val timestampChecker: TimestampChecker) : AbstractNodeService(net) {
object Type : ServiceType("corda.notary") object Type : ServiceType("corda.notary")
private val logger = loggerFor<NotaryService>() abstract val logger: org.slf4j.Logger
/** Implement a factory that specifies the transaction commit protocol for the notary service to use */
abstract val protocolFactory: NotaryProtocol.Factory
init { init {
check(identity.owningKey == signingKey.public) addMessageHandler(NotaryProtocol.TOPIC_INITIATE,
addMessageHandler(NotaryProtocol.TOPIC, { req: NotaryProtocol.Handshake -> processRequest(req) }
{ req: NotaryProtocol.SignRequest -> processRequest(req.txBits, req.callerIdentity) },
{ message, e -> logger.error("Exception during notary service request processing", e) }
) )
} }
/** private fun processRequest(req: NotaryProtocol.Handshake) {
* Checks that the timestamp command is valid (if present) and commits the input state, or returns a conflict val protocol = protocolFactory.create(req.replyTo as SingleMessageRecipient,
* if any of the input states have been previously committed req.sessionID!!,
* req.sendSessionID,
* Note that the transaction is not checked for contract-validity, as that would require fully resolving it timestampChecker,
* into a [TransactionForVerification], for which the caller would have to reveal the whole transaction history chain. uniquenessProvider)
* As a result, the Notary _will commit invalid transactions_ as well, but as it also records the identity of smm.add(NotaryProtocol.TOPIC, protocol)
* the caller, it is possible to raise a dispute and verify the validity of the transaction and subsequently
* undo the commit of the input states (the exact mechanism still needs to be worked out)
*
* TODO: the notary service should only be able to see timestamp commands and inputs
*/
fun processRequest(txBits: SerializedBytes<WireTransaction>, reqIdentity: Party): NotaryProtocol.Result {
val wtx = txBits.deserialize()
try {
validateTimestamp(wtx)
commitInputStates(wtx, reqIdentity)
} catch(e: NotaryException) {
return NotaryProtocol.Result.withError(e.error)
}
val sig = sign(txBits)
return NotaryProtocol.Result.noError(sig)
}
private fun validateTimestamp(tx: WireTransaction) {
val timestampCmd = try {
tx.commands.noneOrSingle { it.value is TimestampCommand } ?: return
} catch (e: IllegalArgumentException) {
throw NotaryException(NotaryError.MoreThanOneTimestamp())
}
if (!timestampCmd.signers.contains(identity.owningKey))
throw NotaryException(NotaryError.NotForMe())
if (!timestampChecker.isValid(timestampCmd.value as TimestampCommand))
throw NotaryException(NotaryError.TimestampInvalid())
}
private fun commitInputStates(tx: WireTransaction, reqIdentity: Party) {
try {
uniquenessProvider.commit(tx, reqIdentity)
} catch (e: UniquenessException) {
val conflictData = e.error.serialize()
val signedConflict = SignedData(conflictData, sign(conflictData))
throw NotaryException(NotaryError.Conflict(tx, signedConflict))
} }
} }
private fun <T : Any> sign(bits: SerializedBytes<T>): DigitalSignature.LegallyIdentifiable {
return signingKey.signWithECDSA(bits, identity)
}
}

View File

@ -0,0 +1,22 @@
package com.r3corda.node.services.transactions
import com.r3corda.core.messaging.MessagingService
import com.r3corda.core.node.services.ServiceType
import com.r3corda.core.node.services.TimestampChecker
import com.r3corda.core.node.services.UniquenessProvider
import com.r3corda.core.utilities.loggerFor
import com.r3corda.node.services.statemachine.StateMachineManager
import com.r3corda.protocols.NotaryProtocol
/** A simple Notary service that does not perform transaction validation */
class SimpleNotaryService(
smm: StateMachineManager,
net: MessagingService,
timestampChecker: TimestampChecker,
uniquenessProvider: UniquenessProvider) : NotaryService(smm, net, timestampChecker, uniquenessProvider) {
object Type : ServiceType("corda.notary.simple")
override val logger = loggerFor<SimpleNotaryService>()
override val protocolFactory = NotaryProtocol.DefaultFactory
}

View File

@ -0,0 +1,33 @@
package com.r3corda.node.services.transactions
import com.r3corda.core.messaging.MessagingService
import com.r3corda.core.messaging.SingleMessageRecipient
import com.r3corda.core.node.services.ServiceType
import com.r3corda.core.node.services.TimestampChecker
import com.r3corda.core.node.services.UniquenessProvider
import com.r3corda.core.utilities.loggerFor
import com.r3corda.node.services.statemachine.StateMachineManager
import com.r3corda.protocols.NotaryProtocol
import com.r3corda.protocols.ValidatingNotaryProtocol
/** A Notary service that validates the transaction chain of he submitted transaction before committing it */
class ValidatingNotaryService(
smm: StateMachineManager,
net: MessagingService,
timestampChecker: TimestampChecker,
uniquenessProvider: UniquenessProvider
) : NotaryService(smm, net, timestampChecker, uniquenessProvider) {
object Type : ServiceType("corda.notary.validating")
override val logger = loggerFor<ValidatingNotaryService>()
override val protocolFactory = object : NotaryProtocol.Factory {
override fun create(otherSide: SingleMessageRecipient,
sendSessionID: Long,
receiveSessionID: Long,
timestampChecker: TimestampChecker,
uniquenessProvider: UniquenessProvider): NotaryProtocol.Service {
return ValidatingNotaryProtocol(otherSide, sendSessionID, receiveSessionID, timestampChecker, uniquenessProvider)
}
}
}

View File

@ -8,6 +8,7 @@ import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.services.Wallet import com.r3corda.core.node.services.Wallet
import com.r3corda.core.node.services.WalletService import com.r3corda.core.node.services.WalletService
import com.r3corda.core.serialization.SingletonSerializeAsToken
import com.r3corda.core.utilities.loggerFor import com.r3corda.core.utilities.loggerFor
import com.r3corda.core.utilities.trace import com.r3corda.core.utilities.trace
import com.r3corda.node.services.api.ServiceHubInternal import com.r3corda.node.services.api.ServiceHubInternal
@ -21,7 +22,7 @@ import javax.annotation.concurrent.ThreadSafe
* states relevant to us into a database and once such a wallet is implemented, this scaffolding can be removed. * states relevant to us into a database and once such a wallet is implemented, this scaffolding can be removed.
*/ */
@ThreadSafe @ThreadSafe
class NodeWalletService(private val services: ServiceHubInternal) : WalletService { class NodeWalletService(private val services: ServiceHubInternal) : SingletonSerializeAsToken(), WalletService {
private val log = loggerFor<NodeWalletService>() private val log = loggerFor<NodeWalletService>()
// Variables inside InnerState are protected with a lock by the ThreadBox and aren't in scope unless you're // Variables inside InnerState are protected with a lock by the ThreadBox and aren't in scope unless you're
@ -129,7 +130,7 @@ class NodeWalletService(private val services: ServiceHubInternal) : WalletServic
m.register("WalletBalances.${balance.key}Pennies", newMetric) m.register("WalletBalances.${balance.key}Pennies", newMetric)
newMetric newMetric
} }
metric.pennies = balance.value.pennies metric.pennies = balance.value.quantity
} }
} }
@ -171,7 +172,7 @@ class NodeWalletService(private val services: ServiceHubInternal) : WalletServic
private fun calculateRandomlySizedAmounts(howMuch: Amount<Currency>, min: Int, max: Int, rng: Random): LongArray { private fun calculateRandomlySizedAmounts(howMuch: Amount<Currency>, min: Int, max: Int, rng: Random): LongArray {
val numStates = min + Math.floor(rng.nextDouble() * (max - min)).toInt() val numStates = min + Math.floor(rng.nextDouble() * (max - min)).toInt()
val amounts = LongArray(numStates) val amounts = LongArray(numStates)
val baseSize = howMuch.pennies / numStates val baseSize = howMuch.quantity / numStates
var filledSoFar = 0L var filledSoFar = 0L
for (i in 0..numStates - 1) { for (i in 0..numStates - 1) {
if (i < numStates - 1) { if (i < numStates - 1) {
@ -180,7 +181,7 @@ class NodeWalletService(private val services: ServiceHubInternal) : WalletServic
filledSoFar += baseSize filledSoFar += baseSize
} else { } else {
// Handle inexact rounding. // Handle inexact rounding.
amounts[i] = howMuch.pennies - filledSoFar amounts[i] = howMuch.quantity - filledSoFar
} }
} }
return amounts return amounts

View File

@ -2,7 +2,7 @@
"fixedLeg": { "fixedLeg": {
"fixedRatePayer": "Bank A", "fixedRatePayer": "Bank A",
"notional": { "notional": {
"pennies": 2500000000, "quantity": 2500000000,
"token": "USD" "token": "USD"
}, },
"paymentFrequency": "SemiAnnual", "paymentFrequency": "SemiAnnual",
@ -27,7 +27,7 @@
"floatingLeg": { "floatingLeg": {
"floatingRatePayer": "Bank B", "floatingRatePayer": "Bank B",
"notional": { "notional": {
"pennies": 2500000000, "quantity": 2500000000,
"token": "USD" "token": "USD"
}, },
"paymentFrequency": "Quarterly", "paymentFrequency": "Quarterly",
@ -56,7 +56,7 @@
} }
}, },
"calculation": { "calculation": {
"expression": "( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))", "expression": "( fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value)) -(floatingLeg.notional.quantity * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
"floatingLegPaymentSchedule": { "floatingLegPaymentSchedule": {
}, },
"fixedLegPaymentSchedule": { "fixedLegPaymentSchedule": {
@ -67,19 +67,19 @@
"eligibleCurrency": "EUR", "eligibleCurrency": "EUR",
"eligibleCreditSupport": "Cash in an Eligible Currency", "eligibleCreditSupport": "Cash in an Eligible Currency",
"independentAmounts": { "independentAmounts": {
"pennies": 0, "quantity": 0,
"token": "EUR" "token": "EUR"
}, },
"threshold": { "threshold": {
"pennies": 0, "quantity": 0,
"token": "EUR" "token": "EUR"
}, },
"minimumTransferAmount": { "minimumTransferAmount": {
"pennies": 25000000, "quantity": 25000000,
"token": "EUR" "token": "EUR"
}, },
"rounding": { "rounding": {
"pennies": 1000000, "quantity": 1000000,
"token": "EUR" "token": "EUR"
}, },
"valuationDate": "Every Local Business Day", "valuationDate": "Every Local Business Day",

View File

@ -12,11 +12,11 @@ import com.r3corda.node.internal.testing.MockNetwork
import com.r3corda.node.services.config.NodeConfiguration import com.r3corda.node.services.config.NodeConfiguration
import com.r3corda.node.services.network.NetworkMapService import com.r3corda.node.services.network.NetworkMapService
import com.r3corda.node.services.persistence.NodeAttachmentService import com.r3corda.node.services.persistence.NodeAttachmentService
import com.r3corda.node.services.transactions.NotaryService import com.r3corda.node.services.transactions.SimpleNotaryService
import org.junit.Before
import org.junit.Test
import com.r3corda.protocols.FetchAttachmentsProtocol import com.r3corda.protocols.FetchAttachmentsProtocol
import com.r3corda.protocols.FetchDataProtocol import com.r3corda.protocols.FetchDataProtocol
import org.junit.Before
import org.junit.Test
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
@ -100,7 +100,7 @@ class AttachmentTests {
} }
} }
} }
}, true, null, null, NetworkMapService.Type, NotaryService.Type) }, true, null, null, NetworkMapService.Type, SimpleNotaryService.Type)
val n1 = network.createNode(n0.info) val n1 = network.createNode(n0.info)
// Insert an attachment into node zero's store directly. // Insert an attachment into node zero's store directly.

View File

@ -28,11 +28,11 @@ import com.r3corda.node.services.persistence.StorageServiceImpl
import com.r3corda.node.services.statemachine.StateMachineManager import com.r3corda.node.services.statemachine.StateMachineManager
import com.r3corda.node.services.wallet.NodeWalletService import com.r3corda.node.services.wallet.NodeWalletService
import com.r3corda.node.services.wallet.WalletImpl import com.r3corda.node.services.wallet.WalletImpl
import com.r3corda.protocols.TwoPartyTradeProtocol
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import com.r3corda.protocols.TwoPartyTradeProtocol
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.nio.file.Path import java.nio.file.Path
@ -218,6 +218,9 @@ class TwoPartyTradeProtocolTests {
assertEquals(bobFuture.get(), aliceFuture.get()) assertEquals(bobFuture.get(), aliceFuture.get())
assertThat(bobNode.smm.findStateMachines(TwoPartyTradeProtocol.Buyer::class.java)).isEmpty() assertThat(bobNode.smm.findStateMachines(TwoPartyTradeProtocol.Buyer::class.java)).isEmpty()
assertThat(bobNode.checkpointStorage.checkpoints).isEmpty()
assertThat(aliceNode.checkpointStorage.checkpoints).isEmpty()
} }
} }
@ -347,7 +350,7 @@ class TwoPartyTradeProtocolTests {
@Test @Test
fun `dependency with error on buyer side`() { fun `dependency with error on buyer side`() {
transactionGroupFor<ContractState> { transactionGroupFor<ContractState> {
runWithError(true, false, "at least one cash input") runWithError(true, false, "at least one asset input")
} }
} }

View File

@ -5,6 +5,7 @@ import com.r3corda.core.messaging.MessagingService
import com.r3corda.core.node.services.* import com.r3corda.core.node.services.*
import com.r3corda.core.node.services.testing.MockStorageService import com.r3corda.core.node.services.testing.MockStorageService
import com.r3corda.core.testing.MOCK_IDENTITY_SERVICE import com.r3corda.core.testing.MOCK_IDENTITY_SERVICE
import com.r3corda.node.serialization.NodeClock
import com.r3corda.node.services.api.Checkpoint import com.r3corda.node.services.api.Checkpoint
import com.r3corda.node.services.api.CheckpointStorage import com.r3corda.node.services.api.CheckpointStorage
import com.r3corda.node.services.api.MonitoringService import com.r3corda.node.services.api.MonitoringService
@ -40,7 +41,7 @@ class MockServices(
val storage: StorageService? = MockStorageService(), val storage: StorageService? = MockStorageService(),
val mapCache: NetworkMapCache? = MockNetworkMapCache(), val mapCache: NetworkMapCache? = MockNetworkMapCache(),
val mapService: NetworkMapService? = null, val mapService: NetworkMapService? = null,
val overrideClock: Clock? = Clock.systemUTC() val overrideClock: Clock? = NodeClock()
) : ServiceHubInternal { ) : ServiceHubInternal {
override val walletService: WalletService = customWallet ?: NodeWalletService(this) override val walletService: WalletService = customWallet ?: NodeWalletService(this)

View File

@ -1,16 +1,19 @@
package com.r3corda.node.services package com.r3corda.node.services
import com.r3corda.core.contracts.TimestampCommand
import com.r3corda.core.contracts.TransactionBuilder import com.r3corda.core.contracts.TransactionBuilder
import com.r3corda.core.seconds import com.r3corda.core.seconds
import com.r3corda.core.testing.DUMMY_NOTARY import com.r3corda.core.testing.DUMMY_NOTARY
import com.r3corda.core.testing.DUMMY_NOTARY_KEY import com.r3corda.core.testing.DUMMY_NOTARY_KEY
import com.r3corda.node.internal.testing.MockNetwork import com.r3corda.node.internal.testing.MockNetwork
import com.r3corda.node.testutils.issueState import com.r3corda.node.internal.testing.issueState
import org.junit.Before import com.r3corda.node.services.network.NetworkMapService
import org.junit.Test import com.r3corda.node.services.transactions.SimpleNotaryService
import com.r3corda.protocols.NotaryError import com.r3corda.protocols.NotaryError
import com.r3corda.protocols.NotaryException import com.r3corda.protocols.NotaryException
import com.r3corda.protocols.NotaryProtocol import com.r3corda.protocols.NotaryProtocol
import org.junit.Before
import org.junit.Test
import java.time.Instant import java.time.Instant
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -22,12 +25,14 @@ class NotaryServiceTests {
lateinit var notaryNode: MockNetwork.MockNode lateinit var notaryNode: MockNetwork.MockNode
lateinit var clientNode: MockNetwork.MockNode lateinit var clientNode: MockNetwork.MockNode
@Before @Before fun setup() {
fun setup() {
// TODO: Move into MockNetwork
net = MockNetwork() net = MockNetwork()
notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY) notaryNode = net.createNode(
clientNode = net.createPartyNode(networkMapAddr = notaryNode.info) legalName = DUMMY_NOTARY.name,
keyPair = DUMMY_NOTARY_KEY,
advertisedServices = *arrayOf(NetworkMapService.Type, SimpleNotaryService.Type)
)
clientNode = net.createNode(networkMapAddress = notaryNode.info)
net.runNetwork() // Clear network map registration messages net.runNetwork() // Clear network map registration messages
} }
@ -35,9 +40,9 @@ class NotaryServiceTests {
val inputState = issueState(clientNode) val inputState = issueState(clientNode)
val tx = TransactionBuilder().withItems(inputState) val tx = TransactionBuilder().withItems(inputState)
tx.setTime(Instant.now(), DUMMY_NOTARY, 30.seconds) tx.setTime(Instant.now(), DUMMY_NOTARY, 30.seconds)
var wtx = tx.toWireTransaction() val wtx = tx.toWireTransaction()
val protocol = NotaryProtocol(wtx, NotaryProtocol.Companion.tracker()) val protocol = NotaryProtocol.Client(wtx)
val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol) val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol)
net.runNetwork() net.runNetwork()
@ -49,7 +54,7 @@ class NotaryServiceTests {
val inputState = issueState(clientNode) val inputState = issueState(clientNode)
val wtx = TransactionBuilder().withItems(inputState).toWireTransaction() val wtx = TransactionBuilder().withItems(inputState).toWireTransaction()
val protocol = NotaryProtocol(wtx, NotaryProtocol.Companion.tracker()) val protocol = NotaryProtocol.Client(wtx)
val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol) val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol)
net.runNetwork() net.runNetwork()
@ -61,9 +66,9 @@ class NotaryServiceTests {
val inputState = issueState(clientNode) val inputState = issueState(clientNode)
val tx = TransactionBuilder().withItems(inputState) val tx = TransactionBuilder().withItems(inputState)
tx.setTime(Instant.now().plusSeconds(3600), DUMMY_NOTARY, 30.seconds) tx.setTime(Instant.now().plusSeconds(3600), DUMMY_NOTARY, 30.seconds)
var wtx = tx.toWireTransaction() val wtx = tx.toWireTransaction()
val protocol = NotaryProtocol(wtx, NotaryProtocol.Companion.tracker()) val protocol = NotaryProtocol.Client(wtx)
val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol) val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol)
net.runNetwork() net.runNetwork()
@ -72,14 +77,32 @@ class NotaryServiceTests {
assertTrue(error is NotaryError.TimestampInvalid) assertTrue(error is NotaryError.TimestampInvalid)
} }
@Test fun `should report error for transaction with more than one timestamp`() {
val inputState = issueState(clientNode)
val tx = TransactionBuilder().withItems(inputState)
val timestamp = TimestampCommand(Instant.now(), 30.seconds)
tx.addCommand(timestamp, DUMMY_NOTARY.owningKey)
tx.addCommand(timestamp, DUMMY_NOTARY.owningKey)
val wtx = tx.toWireTransaction()
val protocol = NotaryProtocol.Client(wtx)
val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol)
net.runNetwork()
val ex = assertFailsWith(ExecutionException::class) { future.get() }
val error = (ex.cause as NotaryException).error
assertTrue(error is NotaryError.MoreThanOneTimestamp)
}
@Test fun `should report conflict for a duplicate transaction`() { @Test fun `should report conflict for a duplicate transaction`() {
val inputState = issueState(clientNode) val inputState = issueState(clientNode)
val wtx = TransactionBuilder().withItems(inputState).toWireTransaction() val wtx = TransactionBuilder().withItems(inputState).toWireTransaction()
val firstSpend = NotaryProtocol(wtx) val firstSpend = NotaryProtocol.Client(wtx)
val secondSpend = NotaryProtocol(wtx) val secondSpend = NotaryProtocol.Client(wtx)
clientNode.smm.add("${NotaryProtocol.TOPIC}.first", firstSpend) clientNode.smm.add("${NotaryProtocol.TOPIC}.first", firstSpend)
val future = clientNode.smm.add("${NotaryProtocol.TOPIC}.second", secondSpend) val future = clientNode.smm.add("${NotaryProtocol.TOPIC}.second", secondSpend)
net.runNetwork() net.runNetwork()
val ex = assertFailsWith(ExecutionException::class) { future.get() } val ex = assertFailsWith(ExecutionException::class) { future.get() }

View File

@ -93,6 +93,6 @@ class PerFileCheckpointStorageTests {
} }
private var checkpointCount = 1 private var checkpointCount = 1
private fun newCheckpoint() = Checkpoint(SerializedBytes(Ints.toByteArray(checkpointCount++)), "topic", "javaType") private fun newCheckpoint() = Checkpoint(SerializedBytes(Ints.toByteArray(checkpointCount++)), "topic", "javaType", null)
} }

View File

@ -1,8 +1,8 @@
package com.r3corda.node.services package com.r3corda.node.services
import com.r3corda.core.contracts.TimestampCommand import com.r3corda.core.contracts.TimestampCommand
import com.r3corda.core.node.services.TimestampChecker
import com.r3corda.core.seconds import com.r3corda.core.seconds
import com.r3corda.node.services.transactions.TimestampChecker
import org.junit.Test import org.junit.Test
import java.time.Clock import java.time.Clock
import java.time.Instant import java.time.Instant

View File

@ -0,0 +1,47 @@
package com.r3corda.node.services
import com.r3corda.core.contracts.TransactionBuilder
import com.r3corda.core.testing.DUMMY_NOTARY
import com.r3corda.core.testing.DUMMY_NOTARY_KEY
import com.r3corda.node.internal.testing.MockNetwork
import com.r3corda.node.internal.testing.issueInvalidState
import com.r3corda.node.services.network.NetworkMapService
import com.r3corda.node.services.transactions.ValidatingNotaryService
import com.r3corda.protocols.NotaryError
import com.r3corda.protocols.NotaryException
import com.r3corda.protocols.NotaryProtocol
import org.junit.Before
import org.junit.Test
import java.util.concurrent.ExecutionException
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
class ValidatingNotaryServiceTests {
lateinit var net: MockNetwork
lateinit var notaryNode: MockNetwork.MockNode
lateinit var clientNode: MockNetwork.MockNode
@Before fun setup() {
net = MockNetwork()
notaryNode = net.createNode(
legalName = DUMMY_NOTARY.name,
keyPair = DUMMY_NOTARY_KEY,
advertisedServices = *arrayOf(NetworkMapService.Type, ValidatingNotaryService.Type)
)
clientNode = net.createNode(networkMapAddress = notaryNode.info)
net.runNetwork() // Clear network map registration messages
}
@Test fun `should report error for invalid transaction dependency`() {
val inputState = issueInvalidState(clientNode)
val wtx = TransactionBuilder().withItems(inputState).toWireTransaction()
val protocol = NotaryProtocol.Client(wtx)
val future = clientNode.smm.add(NotaryProtocol.TOPIC, protocol)
net.runNetwork()
val ex = assertFailsWith(ExecutionException::class) { future.get() }
val notaryError = (ex.cause as NotaryException).error
assertTrue(notaryError is NotaryError.TransactionInvalid, "Received wrong Notary error")
}
}

View File

@ -2,8 +2,8 @@
"fixedLeg": { "fixedLeg": {
"fixedRatePayer": "Bank A", "fixedRatePayer": "Bank A",
"notional": { "notional": {
"pennies": 2500000000, "quantity": 2500000000,
"currency": "USD" "token": "USD"
}, },
"paymentFrequency": "SemiAnnual", "paymentFrequency": "SemiAnnual",
"effectiveDate": "2016-03-11", "effectiveDate": "2016-03-11",
@ -27,8 +27,8 @@
"floatingLeg": { "floatingLeg": {
"floatingRatePayer": "Bank B", "floatingRatePayer": "Bank B",
"notional": { "notional": {
"pennies": 2500000000, "quantity": 2500000000,
"currency": "USD" "token": "USD"
}, },
"paymentFrequency": "Quarterly", "paymentFrequency": "Quarterly",
"effectiveDate": "2016-03-11", "effectiveDate": "2016-03-11",
@ -56,7 +56,7 @@
} }
}, },
"calculation": { "calculation": {
"expression": "( fixedLeg.notional.pennies * (fixedLeg.fixedRate.ratioUnit.value)) -(floatingLeg.notional.pennies * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))", "expression": "( fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value)) -(floatingLeg.notional.quantity * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
"floatingLegPaymentSchedule": { "floatingLegPaymentSchedule": {
}, },
"fixedLegPaymentSchedule": { "fixedLegPaymentSchedule": {
@ -67,20 +67,20 @@
"eligibleCurrency": "EUR", "eligibleCurrency": "EUR",
"eligibleCreditSupport": "Cash in an Eligible Currency", "eligibleCreditSupport": "Cash in an Eligible Currency",
"independentAmounts": { "independentAmounts": {
"pennies": 0, "quantity": 0,
"currency": "EUR" "token": "EUR"
}, },
"threshold": { "threshold": {
"pennies": 0, "quantity": 0,
"currency": "EUR" "token": "EUR"
}, },
"minimumTransferAmount": { "minimumTransferAmount": {
"pennies": 25000000, "quantity": 25000000,
"currency": "EUR" "token": "EUR"
}, },
"rounding": { "rounding": {
"pennies": 1000000, "quantity": 1000000,
"currency": "EUR" "token": "EUR"
}, },
"valuationDate": "Every Local Business Day", "valuationDate": "Every Local Business Day",
"notificationTime": "2:00pm London", "notificationTime": "2:00pm London",
@ -96,7 +96,7 @@
"addressForTransfers": "", "addressForTransfers": "",
"exposure": {}, "exposure": {},
"localBusinessDay": [ "London" , "NewYork" ], "localBusinessDay": [ "London" , "NewYork" ],
"dailyInterestAmount": "(CashAmount * InterestRate ) / (fixedLeg.notional.currency.currencyCode.equals('GBP')) ? 365 : 360", "dailyInterestAmount": "(CashAmount * InterestRate ) / (fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360",
"tradeID": "tradeXXX", "tradeID": "tradeXXX",
"hashLegalDocs": "put hash here" "hashLegalDocs": "put hash here"
}, },

View File

@ -1,5 +1,8 @@
package com.r3corda.demos package com.r3corda.demos
import com.r3corda.core.serialization.SerializeAsToken
import com.r3corda.core.serialization.SerializeAsTokenContext
import com.r3corda.core.serialization.SingletonSerializationToken
import com.r3corda.node.utilities.MutableClock import com.r3corda.node.utilities.MutableClock
import java.time.* import java.time.*
import javax.annotation.concurrent.ThreadSafe import javax.annotation.concurrent.ThreadSafe
@ -8,7 +11,11 @@ import javax.annotation.concurrent.ThreadSafe
* A [Clock] that can have the date advanced for use in demos * A [Clock] that can have the date advanced for use in demos
*/ */
@ThreadSafe @ThreadSafe
class DemoClock(private var delegateClock: Clock = Clock.systemUTC()) : MutableClock() { class DemoClock(private var delegateClock: Clock = Clock.systemUTC()) : MutableClock(), SerializeAsToken {
private val token = SingletonSerializationToken(this)
override fun toToken(context: SerializeAsTokenContext) = SingletonSerializationToken.registerWithContext(token, this, context)
@Synchronized fun updateDate(date: LocalDate): Boolean { @Synchronized fun updateDate(date: LocalDate): Boolean {
val currentDate = LocalDate.now(this) val currentDate = LocalDate.now(this)
@ -25,8 +32,9 @@ class DemoClock(private var delegateClock: Clock = Clock.systemUTC()) : MutableC
return delegateClock.instant() return delegateClock.instant()
} }
@Synchronized override fun withZone(zone: ZoneId): Clock { // Do not use this. Instead seek to use ZonedDateTime methods.
return DemoClock(delegateClock.withZone(zone)) override fun withZone(zone: ZoneId): Clock {
throw UnsupportedOperationException("Tokenized clock does not support withZone()")
} }
@Synchronized override fun getZone(): ZoneId { @Synchronized override fun getZone(): ZoneId {

View File

@ -1,24 +1,24 @@
package com.r3corda.demos package com.r3corda.demos
import com.google.common.net.HostAndPort import com.google.common.net.HostAndPort
import com.typesafe.config.ConfigFactory
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.logElapsedTime import com.r3corda.core.logElapsedTime
import com.r3corda.node.internal.Node
import com.r3corda.node.services.config.NodeConfiguration
import com.r3corda.node.services.config.NodeConfigurationFromConfig
import com.r3corda.core.node.NodeInfo import com.r3corda.core.node.NodeInfo
import com.r3corda.node.services.network.NetworkMapService
import com.r3corda.node.services.clientapi.NodeInterestRates
import com.r3corda.node.services.transactions.NotaryService
import com.r3corda.core.node.services.ServiceType import com.r3corda.core.node.services.ServiceType
import com.r3corda.node.services.messaging.ArtemisMessagingService
import com.r3corda.core.serialization.deserialize import com.r3corda.core.serialization.deserialize
import com.r3corda.core.utilities.BriefLogFormatter import com.r3corda.core.utilities.BriefLogFormatter
import com.r3corda.demos.api.InterestRateSwapAPI import com.r3corda.demos.api.InterestRateSwapAPI
import com.r3corda.demos.protocols.AutoOfferProtocol import com.r3corda.demos.protocols.AutoOfferProtocol
import com.r3corda.demos.protocols.ExitServerProtocol import com.r3corda.demos.protocols.ExitServerProtocol
import com.r3corda.demos.protocols.UpdateBusinessDayProtocol import com.r3corda.demos.protocols.UpdateBusinessDayProtocol
import com.r3corda.node.internal.Node
import com.r3corda.node.services.clientapi.NodeInterestRates
import com.r3corda.node.services.config.NodeConfiguration
import com.r3corda.node.services.config.NodeConfigurationFromConfig
import com.r3corda.node.services.messaging.ArtemisMessagingService
import com.r3corda.node.services.network.NetworkMapService
import com.r3corda.node.services.transactions.SimpleNotaryService
import com.typesafe.config.ConfigFactory
import joptsimple.OptionParser import joptsimple.OptionParser
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
@ -65,12 +65,12 @@ fun main(args: Array<String>) {
val networkMapId = if (options.valueOf(networkMapNetAddr).equals(options.valueOf(networkAddressArg))) { val networkMapId = if (options.valueOf(networkMapNetAddr).equals(options.valueOf(networkAddressArg))) {
// This node provides network map and notary services // This node provides network map and notary services
advertisedServices = setOf(NetworkMapService.Type, NotaryService.Type) advertisedServices = setOf(NetworkMapService.Type, SimpleNotaryService.Type)
null null
} else { } else {
advertisedServices = setOf(NodeInterestRates.Type) advertisedServices = setOf(NodeInterestRates.Type)
try { try {
nodeInfo(options.valueOf(networkMapNetAddr), options.valueOf(networkMapIdentityFile), setOf(NetworkMapService.Type, NotaryService.Type)) nodeInfo(options.valueOf(networkMapNetAddr), options.valueOf(networkMapIdentityFile), setOf(NetworkMapService.Type, SimpleNotaryService.Type))
} catch (e: Exception) { } catch (e: Exception) {
null null
} }

View File

@ -24,8 +24,7 @@ import com.r3corda.node.services.config.NodeConfigurationFromConfig
import com.r3corda.node.services.messaging.ArtemisMessagingService import com.r3corda.node.services.messaging.ArtemisMessagingService
import com.r3corda.node.services.network.NetworkMapService import com.r3corda.node.services.network.NetworkMapService
import com.r3corda.node.services.persistence.NodeAttachmentService import com.r3corda.node.services.persistence.NodeAttachmentService
import com.r3corda.node.services.statemachine.StateMachineManager import com.r3corda.node.services.transactions.SimpleNotaryService
import com.r3corda.node.services.transactions.NotaryService
import com.r3corda.node.services.wallet.NodeWalletService import com.r3corda.node.services.wallet.NodeWalletService
import com.r3corda.node.utilities.ANSIProgressRenderer import com.r3corda.node.utilities.ANSIProgressRenderer
import com.r3corda.protocols.NotaryProtocol import com.r3corda.protocols.NotaryProtocol
@ -115,7 +114,7 @@ fun main(args: Array<String>) {
// the map is not very helpful, but we need one anyway. So just make the buyer side run the network map as it's // the map is not very helpful, but we need one anyway. So just make the buyer side run the network map as it's
// the side that sticks around waiting for the seller. // the side that sticks around waiting for the seller.
val networkMapId = if (role == Role.BUYER) { val networkMapId = if (role == Role.BUYER) {
advertisedServices = setOf(NetworkMapService.Type, NotaryService.Type) advertisedServices = setOf(NetworkMapService.Type, SimpleNotaryService.Type)
null null
} else { } else {
// In a real system, the identity file of the network map would be shipped with the server software, and there'd // In a real system, the identity file of the network map would be shipped with the server software, and there'd
@ -128,9 +127,6 @@ fun main(args: Array<String>) {
NodeInfo(ArtemisMessagingService.makeRecipient(theirNetAddr), party, setOf(NetworkMapService.Type)) NodeInfo(ArtemisMessagingService.makeRecipient(theirNetAddr), party, setOf(NetworkMapService.Type))
} }
// TODO: Remove this once checkpoint resume works.
StateMachineManager.restoreCheckpointsOnStart = false
// And now construct then start the node object. It takes a little while. // And now construct then start the node object. It takes a little while.
val node = logElapsedTime("Node startup") { val node = logElapsedTime("Node startup") {
Node(directory, myNetAddr, config, networkMapId, advertisedServices).start() Node(directory, myNetAddr, config, networkMapId, advertisedServices).start()
@ -175,10 +171,18 @@ fun runSeller(myNetAddr: HostAndPort, node: Node, theirNetAddr: HostAndPort) {
} }
} }
if (node.isPreviousCheckpointsPresent) {
node.smm.findStateMachines(TraderDemoProtocolSeller::class.java).forEach {
ANSIProgressRenderer.progressTracker = it.first.progressTracker
it.second.get()
}
} else {
val otherSide = ArtemisMessagingService.makeRecipient(theirNetAddr) val otherSide = ArtemisMessagingService.makeRecipient(theirNetAddr)
val seller = TraderDemoProtocolSeller(myNetAddr, otherSide) val seller = TraderDemoProtocolSeller(myNetAddr, otherSide)
ANSIProgressRenderer.progressTracker = seller.progressTracker ANSIProgressRenderer.progressTracker = seller.progressTracker
node.smm.add("demo.seller", seller).get() node.smm.add("demo.seller", seller).get()
}
node.stop() node.stop()
} }
@ -190,11 +194,18 @@ fun runBuyer(node: Node) {
it.storePath it.storePath
} }
val future = if (node.isPreviousCheckpointsPresent) {
val (buyer, future) = node.smm.findStateMachines(TraderDemoProtocolBuyer::class.java).single()
ANSIProgressRenderer.progressTracker = buyer.progressTracker //TODO the SMM will soon be able to wire up the ANSIProgressRenderer automatially
future
} else {
// We use a simple scenario-specific wrapper protocol to make things happen. // We use a simple scenario-specific wrapper protocol to make things happen.
val buyer = TraderDemoProtocolBuyer(attachmentsPath, node.info.identity) val buyer = TraderDemoProtocolBuyer(attachmentsPath, node.info.identity)
ANSIProgressRenderer.progressTracker = buyer.progressTracker ANSIProgressRenderer.progressTracker = buyer.progressTracker
// This thread will halt forever here. node.smm.add("demo.buyer", buyer)
node.smm.add("demo.buyer", buyer).get() }
future.get() // This thread will halt forever here.
} }
// We create a couple of ad-hoc test protocols that wrap the two party trade protocol, to give us the demo logic. // We create a couple of ad-hoc test protocols that wrap the two party trade protocol, to give us the demo logic.
@ -339,7 +350,7 @@ class TraderDemoProtocolSeller(val myAddress: HostAndPort,
tx.signWith(keyPair) tx.signWith(keyPair)
// Get the notary to sign it, thus committing the outputs. // Get the notary to sign it, thus committing the outputs.
val notarySig = subProtocol(NotaryProtocol(tx.toWireTransaction())) val notarySig = subProtocol(NotaryProtocol.Client(tx.toWireTransaction()))
tx.addSignatureUnchecked(notarySig) tx.addSignatureUnchecked(notarySig)
// Commit it to local storage. // Commit it to local storage.
@ -354,7 +365,7 @@ class TraderDemoProtocolSeller(val myAddress: HostAndPort,
val builder = TransactionBuilder() val builder = TransactionBuilder()
CommercialPaper().generateMove(builder, issuance.tx.outRef(0), ownedBy) CommercialPaper().generateMove(builder, issuance.tx.outRef(0), ownedBy)
builder.signWith(keyPair) builder.signWith(keyPair)
builder.addSignatureUnchecked(subProtocol(NotaryProtocol(builder.toWireTransaction()))) builder.addSignatureUnchecked(subProtocol(NotaryProtocol.Client(builder.toWireTransaction())))
val tx = builder.toSignedTransaction(true) val tx = builder.toSignedTransaction(true)
serviceHub.recordTransactions(listOf(tx)) serviceHub.recordTransactions(listOf(tx))
tx tx