diff --git a/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt b/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt index 8c9e056bfc..87ea9ad17a 100644 --- a/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt +++ b/confidential-identities/src/main/kotlin/net/corda/confidential/IdentitySyncFlow.kt @@ -12,6 +12,7 @@ package net.corda.confidential import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.ContractState +import net.corda.core.contracts.TransactionResolutionException import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession import net.corda.core.identity.AbstractParty @@ -63,7 +64,15 @@ object IdentitySyncFlow { } private fun extractOurConfidentialIdentities(): Map { - val states: List = (serviceHub.loadStates(tx.inputs.toSet()).map { it.state.data } + tx.outputs.map { it.data }) + val inputStates: List = (tx.inputs.toSet()).mapNotNull { + try { + serviceHub.loadState(it).data + } + catch (e: TransactionResolutionException) { + null + } + } + val states: List = inputStates + tx.outputs.map { it.data } val identities: Set = states.flatMap(ContractState::participants).toSet() // Filter participants down to the set of those not in the network map (are not well known) val confidentialIdentities = identities diff --git a/core/src/main/kotlin/net/corda/core/contracts/ComponentGroupEnum.kt b/core/src/main/kotlin/net/corda/core/contracts/ComponentGroupEnum.kt index e8778203b0..507d60db93 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ComponentGroupEnum.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ComponentGroupEnum.kt @@ -21,5 +21,6 @@ enum class ComponentGroupEnum { ATTACHMENTS_GROUP, // ordinal = 3. NOTARY_GROUP, // ordinal = 4. TIMEWINDOW_GROUP, // ordinal = 5. - SIGNERS_GROUP // ordinal = 6. + SIGNERS_GROUP, // ordinal = 6. + REFERENCES_GROUP // ordinal = 7. } diff --git a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt index 604c347727..b889792f28 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -179,9 +179,15 @@ data class StateRef(val txhash: SecureHash, val index: Int) { @KeepForDJVM @CordaSerializable // DOCSTART 7 -data class StateAndRef(val state: TransactionState, val ref: StateRef) +data class StateAndRef(val state: TransactionState, val ref: StateRef) { + /** For adding [StateAndRef]s as references to a [TransactionBuilder]. */ + fun referenced() = ReferencedStateAndRef(this) +} // DOCEND 7 +/** A wrapper for a [StateAndRef] indicating that it should be added to a transaction as a reference input state. */ +data class ReferencedStateAndRef(val stateAndRef: StateAndRef) + /** Filters a list of [StateAndRef] objects according to the type of the states */ inline fun Iterable>.filterStatesOfType(): List> { return mapNotNull { if (it.state.data is T) StateAndRef(TransactionState(it.state.data, it.state.contract, it.state.notary), it.ref) else null } diff --git a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt index edd93b164c..681f085644 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FinalityFlow.kt @@ -102,7 +102,7 @@ class FinalityFlow(val transaction: SignedTransaction, private fun needsNotarySignature(stx: SignedTransaction): Boolean { val wtx = stx.tx - val needsNotarisation = wtx.inputs.isNotEmpty() || wtx.timeWindow != null + val needsNotarisation = wtx.inputs.isNotEmpty() || wtx.references.isNotEmpty() || wtx.timeWindow != null return needsNotarisation && hasNoNotarySignature(stx) } diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt index d61201bdf6..4a50278208 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt @@ -13,6 +13,7 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.strands.Strand import net.corda.core.CordaInternal +import net.corda.core.contracts.StateRef import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party import net.corda.core.identity.PartyAndCertificate @@ -30,6 +31,7 @@ import net.corda.core.utilities.debug import net.corda.core.utilities.toNonEmptySet import org.slf4j.Logger import java.time.Duration +import java.util.* /** * A sub-class of [FlowLogic] implements a flow using direct, straight line blocking code. Thus you @@ -410,6 +412,18 @@ abstract class FlowLogic { return stateMachine.suspend(request, maySkipCheckpoint = maySkipCheckpoint) } + /** + * Suspends the current flow until all the provided [StateRef]s have been consumed. + * + * WARNING! Remember that the flow which uses this async operation will _NOT_ wake-up until all the supplied StateRefs + * have been consumed. If the node isn't aware of the supplied StateRefs or if the StateRefs are never consumed, then + * the calling flow will remain suspended FOREVER!! + * + * @param stateRefs the StateRefs which will be consumed in the future. + */ + @Suspendable + fun waitForStateConsumption(stateRefs: Set) = executeAsync(WaitForStateConsumption(stateRefs, serviceHub)) + /** * Returns a shallow copy of the Quasar stack frames at the time of call to [flowStackSnapshot]. Use this to inspect * what objects would be serialised at the time of call to a suspending action (e.g. send/receive). diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt index 4b082ad10f..03298f1751 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryError.kt @@ -65,11 +65,25 @@ sealed class NotaryError { /** Contains information about the consuming transaction for a particular state. */ // TODO: include notary timestamp? @CordaSerializable -data class StateConsumptionDetails( +data class StateConsumptionDetails @JvmOverloads constructor( /** * Hash of the consuming transaction id. * * Note that this is NOT the transaction id itself – revealing it could lead to privacy leaks. */ - val hashOfTransactionId: SecureHash -) \ No newline at end of file + val hashOfTransactionId: SecureHash, + + /** + * The type of consumed state: Either a reference input state or a regular input state. + */ + val type: ConsumedStateType = ConsumedStateType.INPUT_STATE +) { + + @CordaSerializable + enum class ConsumedStateType { INPUT_STATE, REFERENCE_INPUT_STATE } + + fun copy(hashOfTransactionId: SecureHash): StateConsumptionDetails { + return StateConsumptionDetails(hashOfTransactionId, type) + } +} + diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt index babcc728fd..026f3e33b5 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryFlow.kt @@ -75,8 +75,8 @@ class NotaryFlow { protected fun checkTransaction(): Party { val notaryParty = stx.notary ?: throw IllegalStateException("Transaction does not specify a Notary") check(serviceHub.networkMapCache.isNotary(notaryParty)) { "$notaryParty is not a notary on the network" } - check(serviceHub.loadStates(stx.inputs.toSet()).all { it.state.notary == notaryParty }) { - "Input states must have the same Notary" + check(serviceHub.loadStates(stx.inputs.toSet() + stx.references.toSet()).all { it.state.notary == notaryParty }) { + "Input states and reference input states must have the same Notary" } stx.resolveTransactionWithSignatures(serviceHub).verifySignaturesExcept(notaryParty.owningKey) return notaryParty diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryWireFormat.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryWireFormat.kt index 6c3ca93ec2..e671accd12 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryWireFormat.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryWireFormat.kt @@ -26,6 +26,9 @@ import net.corda.core.transactions.SignedTransaction * While the signature must be retained, the notarisation request does not need to be transferred or stored anywhere - it * can be built from a [SignedTransaction] or a [CoreTransaction]. The notary can recompute it from the committed states index. * + * Reference inputs states are not included as a separate property in the [NotarisationRequest] as they are not + * consumed. + * * In case there is a need to prove that a party spent a particular state, the notary will: * 1) Locate the consuming transaction id in the index, along with all other states consumed in the same transaction. * 2) Build a [NotarisationRequest]. diff --git a/core/src/main/kotlin/net/corda/core/flows/WithReferencedStatesFlow.kt b/core/src/main/kotlin/net/corda/core/flows/WithReferencedStatesFlow.kt new file mode 100644 index 0000000000..39210a6923 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/flows/WithReferencedStatesFlow.kt @@ -0,0 +1,115 @@ +package net.corda.core.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.StateRef +import net.corda.core.internal.uncheckedCast +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.contextLogger + +/** + * Given a flow which uses reference states, the [WithReferencedStatesFlow] will execute the the flow as a subFlow. + * If the flow fails due to a [NotaryError.Conflict] for a reference state, then it will be suspended until the + * state refs for the reference states are consumed. In this case, a consumption means that: + * + * 1. the owner of the reference state has updated the state with a valid, notarised transaction + * 2. the owner of the reference state has shared the update with the node attempting to run the flow which uses the + * reference state + * 3. The node has successfully committed the transaction updating the reference state (and all the dependencies), and + * added the updated reference state to the vault. + * + * WARNING: Caution should be taken when using this flow as it facilitates automated re-running of flows which use + * reference states. The flow using reference states should include checks to ensure that the reference data is + * reasonable, especially if some economics transaction depends upon it. + * + * @param flowLogic a flow which uses reference states. + * @param progressTracker a progress tracker instance. + */ +class WithReferencedStatesFlow( + val flowLogic: FlowLogic, + override val progressTracker: ProgressTracker = WithReferencedStatesFlow.tracker() +) : FlowLogic() { + + companion object { + val logger = contextLogger() + + object ATTEMPT : ProgressTracker.Step("Attempting to run flow which uses reference states.") + object RETRYING : ProgressTracker.Step("Reference states are out of date! Waiting for updated states...") + object SUCCESS : ProgressTracker.Step("Flow ran successfully.") + + @JvmStatic + fun tracker() = ProgressTracker(ATTEMPT, RETRYING, SUCCESS) + } + + private sealed class FlowResult { + data class Success(val value: T) : FlowResult() + data class Conflict(val stateRefs: Set) : FlowResult() + } + + /** + * Process the flow result. We don't care about anything other than NotaryExceptions. If it is a + * NotaryException but not a Conflict, then just rethrow. If it's a Conflict, then extract the reference + * input state refs. Otherwise, if the flow completes successfully then return the result. + */ + private fun processFlowResult(result: Any): FlowResult { + return when (result) { + is NotaryException -> { + val error = result.error + if (error is NotaryError.Conflict) { + val conflictingReferenceStateRefs = error.consumedStates.filter { + it.value.type == StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE + }.map { it.key }.toSet() + FlowResult.Conflict(conflictingReferenceStateRefs) + } else { + throw result + } + } + is FlowException -> throw result + else -> FlowResult.Success(result) + } + } + + @Suspendable + override fun call(): T { + progressTracker.currentStep = ATTEMPT + + // Loop until the flow successfully completes. We need to + // do this because there might be consecutive update races. + while (true) { + // Return a successful flow result or a FlowException. + logger.info("Attempting to run the supplied flow ${flowLogic.javaClass.canonicalName}.") + val result = try { + subFlow(flowLogic) + } catch (e: FlowException) { + e + } + + val processedResult = processFlowResult(result) + + // Return the flow result or wait for the StateRefs to be updated and try again. + // 1. If a success, we can always cast the return type to T. + // 2. If there is a conflict, then suspend this flow, only waking it up when the conflicting reference + // states have been updated. + @Suppress("UNCHECKED_CAST") + when (processedResult) { + is FlowResult.Success<*> -> { + logger.info("Flow ${flowLogic.javaClass.canonicalName} completed successfully.") + progressTracker.currentStep = SUCCESS + return uncheckedCast(processedResult.value) + } + is FlowResult.Conflict -> { + val conflicts = processedResult.stateRefs + logger.info("Flow ${flowLogic.javaClass.name} failed due to reference state conflicts: $conflicts.") + + // Only set currentStep to FAILURE once. + if (progressTracker.currentStep != RETRYING) { + progressTracker.currentStep = RETRYING + } + + // Suspend this flow. + waitForStateConsumption(conflicts) + logger.info("All referenced states have been updated. Retrying flow...") + } + } + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/FlowAsyncOperation.kt b/core/src/main/kotlin/net/corda/core/internal/FlowAsyncOperation.kt index 8831a5c1e7..9d44cbee85 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FlowAsyncOperation.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FlowAsyncOperation.kt @@ -34,4 +34,4 @@ fun FlowLogic.executeAsync(operation: FlowAsyncOperation, may val request = FlowIORequest.ExecuteAsyncOperation(operation) return stateMachine.suspend(request, maySkipCheckpoint) } -// DOCEND executeAsync +// DOCEND executeAsync \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt index bbf96e9a0a..7d141e34a5 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/ResolveTransactionsFlow.kt @@ -52,12 +52,11 @@ class ResolveTransactionsFlow(txHashesArg: Set, @DeleteForDJVM companion object { - private fun dependencyIDs(stx: SignedTransaction) = stx.inputs.map { it.txhash }.toSet() + private fun dependencyIDs(stx: SignedTransaction) = stx.inputs.map { it.txhash }.toSet() + stx.references.map { it.txhash }.toSet() private const val RESOLUTION_PAGE_SIZE = 100 - /** - * Topologically sorts the given transactions such that dependencies are listed before dependers. */ + /** Topologically sorts the given transactions such that dependencies are listed before dependers. */ @JvmStatic fun topologicalSort(transactions: Collection): List { val sort = TopologicalSort() @@ -71,7 +70,7 @@ class ResolveTransactionsFlow(txHashesArg: Set, @CordaSerializable class ExcessivelyLargeTransactionGraph : FlowException() - /** Transaction for fetch attachments for */ + /** Transaction to fetch attachments for. */ private var signedTransaction: SignedTransaction? = null // TODO: Figure out a more appropriate DOS limit here, 5000 is simply a very bad guess. @@ -156,8 +155,8 @@ class ResolveTransactionsFlow(txHashesArg: Set, for (stx in downloads) check(resultQ.putIfAbsent(stx.id, stx) == null) // Assert checks the filter at the start. - // Add all input states to the work queue. - val inputHashes = downloads.flatMap { it.inputs }.map { it.txhash } + // Add all input states and reference input states to the work queue. + val inputHashes = downloads.flatMap { it.inputs + it.references }.map { it.txhash } nextRequests.addAll(inputHashes) limitCounter = limitCounter exactAdd nextRequests.size diff --git a/core/src/main/kotlin/net/corda/core/internal/TopologicalSort.kt b/core/src/main/kotlin/net/corda/core/internal/TopologicalSort.kt index b0fd2d0abf..c08e5bf118 100644 --- a/core/src/main/kotlin/net/corda/core/internal/TopologicalSort.kt +++ b/core/src/main/kotlin/net/corda/core/internal/TopologicalSort.kt @@ -17,9 +17,10 @@ class TopologicalSort { * Add a transaction to the to-be-sorted set of transactions. */ fun add(stx: SignedTransaction) { - for (input in stx.inputs) { - // Note that we use a LinkedHashSet here to make the traversal deterministic (as long as the input list is) - forwardGraph.getOrPut(input.txhash) { LinkedHashSet() }.add(stx) + val stateRefs = stx.references + stx.inputs + stateRefs.forEach { (txhash) -> + // Note that we use a LinkedHashSet here to make the traversal deterministic (as long as the input list is). + forwardGraph.getOrPut(txhash) { LinkedHashSet() }.add(stx) } transactions.add(stx) } diff --git a/core/src/main/kotlin/net/corda/core/internal/WaitForStateConsumption.kt b/core/src/main/kotlin/net/corda/core/internal/WaitForStateConsumption.kt new file mode 100644 index 0000000000..6c6dcb8605 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/WaitForStateConsumption.kt @@ -0,0 +1,43 @@ +package net.corda.core.internal + +import net.corda.core.concurrent.CordaFuture +import net.corda.core.contracts.StateRef +import net.corda.core.internal.concurrent.asCordaFuture +import net.corda.core.node.ServiceHub +import net.corda.core.utilities.contextLogger +import java.util.concurrent.CompletableFuture + +/** + * An [FlowAsyncOperation] which suspends a flow until the provided [StateRef]s have been updated. + * + * WARNING! Remember that if the node isn't aware of the supplied StateRefs or if the StateRefs are never updated, then + * the calling flow will remain suspended. + * + * @property stateRefs the StateRefs which will be updated. + * @property services a ServiceHub instance + */ +class WaitForStateConsumption(val stateRefs: Set, val services: ServiceHub) : FlowAsyncOperation { + + companion object { + val logger = contextLogger() + } + + override fun execute(): CordaFuture { + val futures = stateRefs.map { services.vaultService.whenConsumed(it).toCompletableFuture() } + val completedFutures = futures.filter { it.isDone } + + when { + completedFutures.isEmpty() -> + logger.info("All StateRefs $stateRefs have yet to be consumed. Suspending...") + futures == completedFutures -> + logger.info("All StateRefs $stateRefs have already been consumed. No need to suspend.") + else -> { + val updatedStateRefs = completedFutures.flatMap { it.get().consumed.map { it.ref } } + val notUpdatedStateRefs = stateRefs - updatedStateRefs + logger.info("StateRefs $notUpdatedStateRefs have yet to be consumed. Suspending...") + } + } + + return CompletableFuture.allOf(*futures.toTypedArray()).thenApply { Unit }.asCordaFuture() + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt index bfe059f189..7a47a2eccc 100644 --- a/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt +++ b/core/src/main/kotlin/net/corda/core/internal/notary/NotaryServiceFlow.kt @@ -37,10 +37,10 @@ abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service: txId = parts.id checkNotary(parts.notary) if (service is AsyncCFTNotaryService) { - val result = executeAsync(AsyncCFTNotaryService.CommitOperation(service, parts.inputs, txId, otherSideSession.counterparty, requestPayload.requestSignature, parts.timestamp)) + val result = executeAsync(AsyncCFTNotaryService.CommitOperation(service, parts.inputs, txId, otherSideSession.counterparty, requestPayload.requestSignature, parts.timestamp, parts.references)) if (result is Result.Failure) throw NotaryInternalException(result.error) } else { - service.commitInputStates(parts.inputs, txId, otherSideSession.counterparty, requestPayload.requestSignature, parts.timestamp) + service.commitInputStates(parts.inputs, txId, otherSideSession.counterparty, requestPayload.requestSignature, parts.timestamp, parts.references) } signTransactionAndSendResponse(txId) } catch (e: NotaryInternalException) { @@ -89,8 +89,18 @@ abstract class NotaryServiceFlow(val otherSideSession: FlowSession, val service: * The minimum amount of information needed to notarise a transaction. Note that this does not include * any sensitive transaction details. */ - protected data class TransactionParts(val id: SecureHash, val inputs: List, val timestamp: TimeWindow?, val notary: Party?) + protected data class TransactionParts @JvmOverloads constructor( + val id: SecureHash, + val inputs: List, + val timestamp: TimeWindow?, + val notary: Party?, + val references: List = emptyList() + ) { + fun copy(id: SecureHash, inputs: List, timestamp: TimeWindow?, notary: Party?): TransactionParts { + return TransactionParts(id, inputs, timestamp, notary, references) + } + } } /** Exception internal to the notary service. Does not get exposed to CorDapps and flows calling [NotaryFlow.Client]. */ -class NotaryInternalException(val error: NotaryError) : FlowException("Unable to notarise: $error") \ No newline at end of file +class NotaryInternalException(val error: NotaryError) : FlowException("Unable to notarise: $error") diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt b/core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt index a947288a97..21a014c36c 100644 --- a/core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt +++ b/core/src/main/kotlin/net/corda/core/internal/notary/TrustedAuthorityNotaryService.kt @@ -25,9 +25,23 @@ abstract class TrustedAuthorityNotaryService : NotaryService() { * A NotaryException is thrown if any of the states have been consumed by a different transaction. Note that * this method does not throw an exception when input states are present multiple times within the transaction. */ - fun commitInputStates(inputs: List, txId: SecureHash, caller: Party, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?) { + @JvmOverloads + fun commitInputStates(inputs: List, txId: SecureHash, caller: Party, requestSignature: NotarisationRequestSignature, timeWindow: TimeWindow?, references: List = emptyList()) { try { - uniquenessProvider.commit(inputs, txId, caller, requestSignature, timeWindow) + uniquenessProvider.commit(inputs, txId, caller, requestSignature, timeWindow, references) + } catch (e: NotaryInternalException) { + if (e.error is NotaryError.Conflict) { + val allInputs = inputs + references + val conflicts = allInputs.filterIndexed { _, stateRef -> + val cause = e.error.consumedStates[stateRef] + cause != null && cause.hashOfTransactionId != txId.sha256() + } + if (conflicts.isNotEmpty()) { + // TODO: Create a new UniquenessException that only contains the conflicts filtered above. + log.info("Notary conflicts for $txId: $conflicts") + throw e + } + } else throw e } catch (e: Exception) { if (e is NotaryInternalException) throw e log.error("Internal error", e) @@ -47,4 +61,4 @@ abstract class TrustedAuthorityNotaryService : NotaryService() { } // TODO: Sign multiple transactions at once by building their Merkle tree and then signing over its root. -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/UniquenessProvider.kt b/core/src/main/kotlin/net/corda/core/internal/notary/UniquenessProvider.kt index 3e31c95978..04d97915c6 100644 --- a/core/src/main/kotlin/net/corda/core/internal/notary/UniquenessProvider.kt +++ b/core/src/main/kotlin/net/corda/core/internal/notary/UniquenessProvider.kt @@ -19,6 +19,7 @@ interface UniquenessProvider { txId: SecureHash, callerIdentity: Party, requestSignature: NotarisationRequestSignature, - timeWindow: TimeWindow? = null + timeWindow: TimeWindow? = null, + references: List = emptyList() ) } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt index bf7bd60d91..833db70277 100644 --- a/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt +++ b/core/src/main/kotlin/net/corda/core/node/NetworkParameters.kt @@ -115,4 +115,4 @@ data class NetworkParameters( */ @KeepForDJVM @CordaSerializable -data class NotaryInfo(val identity: Party, val validating: Boolean) +data class NotaryInfo(val identity: Party, val validating: Boolean) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index 12006608ae..4e131785fb 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -19,6 +19,7 @@ import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.identity.AbstractParty +import net.corda.core.internal.concurrent.doneFuture import net.corda.core.messaging.DataFeed import net.corda.core.node.services.Vault.StateStatus import net.corda.core.node.services.vault.* @@ -190,7 +191,14 @@ interface VaultService { */ @DeleteForDJVM fun whenConsumed(ref: StateRef): CordaFuture> { - return updates.filter { it.consumed.any { it.ref == ref } }.toFuture() + val query = QueryCriteria.VaultQueryCriteria(stateRefs = listOf(ref), status = Vault.StateStatus.CONSUMED) + val result = trackBy(query) + val snapshot = result.snapshot.states + return if (snapshot.isNotEmpty()) { + doneFuture(Vault.Update(consumed = setOf(snapshot.single()), produced = emptySet())) + } else { + result.updates.toFuture() + } } /** diff --git a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt index fa1e568cf1..bc9447ccb5 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt @@ -25,6 +25,8 @@ import java.util.function.Predicate @KeepForDJVM @DoNotImplement abstract class BaseTransaction : NamedByHash { + /** A list of reusable reference data states which can be referred to by other contracts in this transaction. */ + abstract val references: List<*> /** The inputs of this transaction. Note that in BaseTransaction subclasses the type of this list may change! */ abstract val inputs: List<*> /** Ordered list of states defined by this transaction, along with the associated notaries. */ @@ -39,16 +41,26 @@ abstract class BaseTransaction : NamedByHash { protected open fun checkBaseInvariants() { checkNotarySetIfInputsPresent() checkNoDuplicateInputs() + checkForInputsAndReferencesOverlap() } private fun checkNotarySetIfInputsPresent() { - if (inputs.isNotEmpty()) { + if (inputs.isNotEmpty() || references.isNotEmpty()) { check(notary != null) { "The notary must be specified explicitly for any transaction that has inputs" } } } private fun checkNoDuplicateInputs() { check(inputs.size == inputs.toSet().size) { "Duplicate input states detected" } + check(references.size == references.toSet().size) { "Duplicate reference states detected" } + } + + private fun checkForInputsAndReferencesOverlap() { + val intersection = inputs intersect references + require(intersection.isEmpty()) { + "A StateRef cannot be both an input and a reference input in the same transaction. Offending " + + "StateRefs: $intersection" + } } /** diff --git a/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt index 51615e6d86..f232b34b2b 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/BaseTransactions.kt @@ -22,23 +22,26 @@ import net.corda.core.serialization.CordaSerializable */ @CordaSerializable abstract class CoreTransaction : BaseTransaction() { - /** The inputs of this transaction, containing state references only **/ + /** The inputs of this transaction, containing state references only. **/ abstract override val inputs: List + /** The reference inputs of this transaction, containing the state references only. **/ + abstract override val references: List } /** A transaction with fully resolved components, such as input states. */ abstract class FullTransaction : BaseTransaction() { abstract override val inputs: List> + abstract override val references: List> override fun checkBaseInvariants() { super.checkBaseInvariants() - checkInputsHaveSameNotary() + checkInputsAndReferencesHaveSameNotary() } - private fun checkInputsHaveSameNotary() { - if (inputs.isEmpty()) return - val inputNotaries = inputs.map { it.state.notary }.toHashSet() - check(inputNotaries.size == 1) { "All inputs must point to the same notary" } - check(inputNotaries.single() == notary) { "The specified notary must be the one specified by all inputs" } + private fun checkInputsAndReferencesHaveSameNotary() { + if (inputs.isEmpty() && references.isEmpty()) return + val notaries = (inputs + references).map { it.state.notary }.toHashSet() + check(notaries.size == 1) { "All inputs and reference inputs must point to the same notary" } + check(notaries.single() == notary) { "The specified notary must be the one specified by all inputs and input references" } } } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt index dc98b51b7c..e6f2c6c20a 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/ContractUpgradeTransactions.kt @@ -65,8 +65,11 @@ data class ContractUpgradeWireTransaction( get() = throw UnsupportedOperationException("ContractUpgradeWireTransaction does not contain output states, " + "outputs can only be obtained from a resolved ContractUpgradeLedgerTransaction") + /** ContractUpgradeWireTransactions should not contain reference input states. */ + override val references: List get() = emptyList() + override val id: SecureHash by lazy { - val componentHashes =serializedComponents.mapIndexed { index, component -> + val componentHashes = serializedComponents.mapIndexed { index, component -> componentHash(nonces[index], component) } combinedHash(componentHashes) @@ -155,6 +158,7 @@ data class ContractUpgradeFilteredTransaction( combinedHash(hashList) } override val outputs: List> get() = emptyList() + override val references: List get() = emptyList() /** Contains the serialized component and the associated nonce for computing the transaction id. */ @CordaSerializable @@ -183,6 +187,8 @@ data class ContractUpgradeLedgerTransaction( override val sigs: List, private val networkParameters: NetworkParameters ) : FullTransaction(), TransactionWithSignatures { + /** ContractUpgradeLEdgerTransactions do not contain reference input states. */ + override val references: List> = emptyList() /** The legacy contract class name is determined by the first input state. */ private val legacyContractClassName = inputs.first().state.contract private val upgradedContract: UpgradedContract = loadUpgradedContract() diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index aba11c519b..01516cc9aa 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -52,7 +52,8 @@ data class LedgerTransaction @JvmOverloads constructor( override val notary: Party?, val timeWindow: TimeWindow?, val privacySalt: PrivacySalt, - private val networkParameters: NetworkParameters? = null + private val networkParameters: NetworkParameters? = null, + override val references: List> = emptyList() ) : FullTransaction() { //DOCEND 1 init { @@ -76,10 +77,12 @@ data class LedgerTransaction @JvmOverloads constructor( } } + // Input reference state contracts are not required for verification. private val contracts: Map>> = (inputs.map { it.state } + outputs) .map { it.contract to stateToContractClass(it) }.toMap() val inputStates: List get() = inputs.map { it.state.data } + val referenceStates: List get() = references.map { it.state.data } /** * Returns the typed input StateAndRef at the specified index @@ -159,13 +162,13 @@ data class LedgerTransaction @JvmOverloads constructor( /** * Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there - * are any inputs, all outputs must have the same notary. + * are any inputs or reference inputs, all outputs must have the same notary. * * TODO: Is that the correct set of restrictions? May need to come back to this, see if we can be more * flexible on output notaries. */ private fun checkNoNotaryChange() { - if (notary != null && inputs.isNotEmpty()) { + if (notary != null && (inputs.isNotEmpty() || references.isNotEmpty())) { outputs.forEach { if (it.notary != notary) { throw TransactionVerificationException.NotaryChangeInWrongTransactionType(id, notary, it.notary) @@ -264,6 +267,13 @@ data class LedgerTransaction @JvmOverloads constructor( */ fun getInput(index: Int): ContractState = inputs[index].state.data + /** + * Helper to simplify getting an indexed reference input [ContractState]. + * @param index the position of the item in the references. + * @return The [StateAndRef] at the requested index. + */ + fun getReferenceInput(index: Int): ContractState = references[index].state.data + /** * Helper to simplify getting all inputs states of a particular class, interface, or base class. * @param clazz The class type used for filtering via an [Class.isInstance] check. @@ -274,6 +284,16 @@ data class LedgerTransaction @JvmOverloads constructor( inline fun inputsOfType(): List = inputsOfType(T::class.java) + /** + * Helper to simplify getting all reference input states of a particular class, interface, or base class. + * @param clazz The class type used for filtering via an [Class.isInstance] check. + * [clazz] must be an extension of [ContractState]. + * @return the possibly empty list of inputs matching the clazz restriction. + */ + fun referenceInputsOfType(clazz: Class): List = references.mapNotNull { clazz.castIfPossible(it.state.data) } + + inline fun referenceInputsOfType(): List = referenceInputsOfType(T::class.java) + /** * Helper to simplify getting all inputs states of a particular class, interface, or base class. * @param clazz The class type used for filtering via an [Class.isInstance] check. @@ -286,6 +306,18 @@ data class LedgerTransaction @JvmOverloads constructor( inline fun inRefsOfType(): List> = inRefsOfType(T::class.java) + /** + * Helper to simplify getting all reference input states of a particular class, interface, or base class. + * @param clazz The class type used for filtering via an [Class.isInstance] check. + * [clazz] must be an extension of [ContractState]. + * @return the possibly empty list of reference inputs [StateAndRef] matching the clazz restriction. + */ + fun referenceInputRefsOfType(clazz: Class): List> { + return references.mapNotNull { if (clazz.isInstance(it.state.data)) uncheckedCast, StateAndRef>(it) else null } + } + + inline fun referenceInputRefsOfType(): List> = referenceInputRefsOfType(T::class.java) + /** * Helper to simplify filtering inputs according to a [Predicate]. * @param clazz The class type used for filtering via an [Class.isInstance] check. @@ -302,6 +334,22 @@ data class LedgerTransaction @JvmOverloads constructor( return filterInputs(T::class.java, Predicate { predicate(it) }) } + /** + * Helper to simplify filtering reference inputs according to a [Predicate]. + * @param clazz The class type used for filtering via an [Class.isInstance] check. + * [clazz] must be an extension of [ContractState]. + * @param predicate A filtering function taking a state of type T and returning true if it should be included in the list. + * The class filtering is applied before the predicate. + * @return the possibly empty list of reference states matching the predicate and clazz restrictions. + */ + fun filterReferenceInputs(clazz: Class, predicate: Predicate): List { + return referenceInputsOfType(clazz).filter { predicate.test(it) } + } + + inline fun filterReferenceInputs(crossinline predicate: (T) -> Boolean): List { + return filterReferenceInputs(T::class.java, Predicate { predicate(it) }) + } + /** * Helper to simplify filtering inputs according to a [Predicate]. * @param predicate A filtering function taking a state of type T and returning true if it should be included in the list. @@ -318,6 +366,22 @@ data class LedgerTransaction @JvmOverloads constructor( return filterInRefs(T::class.java, Predicate { predicate(it) }) } + /** + * Helper to simplify filtering reference inputs according to a [Predicate]. + * @param predicate A filtering function taking a state of type T and returning true if it should be included in the list. + * The class filtering is applied before the predicate. + * @param clazz The class type used for filtering via an [Class.isInstance] check. + * [clazz] must be an extension of [ContractState]. + * @return the possibly empty list of references [StateAndRef] matching the predicate and clazz restrictions. + */ + fun filterReferenceInputRefs(clazz: Class, predicate: Predicate): List> { + return referenceInputRefsOfType(clazz).filter { predicate.test(it.state.data) } + } + + inline fun filterReferenceInputRefs(crossinline predicate: (T) -> Boolean): List> { + return filterReferenceInputRefs(T::class.java, Predicate { predicate(it) }) + } + /** * Helper to simplify finding a single input [ContractState] matching a [Predicate]. * @param clazz The class type used for filtering via an [Class.isInstance] check. @@ -335,6 +399,23 @@ data class LedgerTransaction @JvmOverloads constructor( return findInput(T::class.java, Predicate { predicate(it) }) } + /** + * Helper to simplify finding a single reference inputs [ContractState] matching a [Predicate]. + * @param clazz The class type used for filtering via an [Class.isInstance] check. + * [clazz] must be an extension of ContractState. + * @param predicate A filtering function taking a state of type T and returning true if this is the desired item. + * The class filtering is applied before the predicate. + * @return the single item matching the predicate. + * @throws IllegalArgumentException if no item, or multiple items are found matching the requirements. + */ + fun findReference(clazz: Class, predicate: Predicate): T { + return referenceInputsOfType(clazz).single { predicate.test(it) } + } + + inline fun findReference(crossinline predicate: (T) -> Boolean): T { + return findReference(T::class.java, Predicate { predicate(it) }) + } + /** * Helper to simplify finding a single input matching a [Predicate]. * @param clazz The class type used for filtering via an [Class.isInstance] check. @@ -352,6 +433,23 @@ data class LedgerTransaction @JvmOverloads constructor( return findInRef(T::class.java, Predicate { predicate(it) }) } + /** + * Helper to simplify finding a single reference input matching a [Predicate]. + * @param clazz The class type used for filtering via an [Class.isInstance] check. + * [clazz] must be an extension of ContractState. + * @param predicate A filtering function taking a state of type T and returning true if this is the desired item. + * The class filtering is applied before the predicate. + * @return the single item matching the predicate. + * @throws IllegalArgumentException if no item, or multiple items are found matching the requirements. + */ + fun findReferenceInputRef(clazz: Class, predicate: Predicate): StateAndRef { + return referenceInputRefsOfType(clazz).single { predicate.test(it.state.data) } + } + + inline fun findReferenceInputRef(crossinline predicate: (T) -> Boolean): StateAndRef { + return findReferenceInputRef(T::class.java, Predicate { predicate(it) }) + } + /** * Helper to simplify getting an indexed command. * @param index the position of the item in the commands. @@ -419,14 +517,26 @@ data class LedgerTransaction @JvmOverloads constructor( */ fun getAttachment(id: SecureHash): Attachment = attachments.first { it.id == id } - fun copy(inputs: List>, - outputs: List>, - commands: List>, - attachments: List, - id: SecureHash, - notary: Party?, - timeWindow: TimeWindow?, - privacySalt: PrivacySalt - ) = copy(inputs = inputs, outputs = outputs, commands = commands, attachments = attachments, id = id, notary = notary, timeWindow = timeWindow, privacySalt = privacySalt, networkParameters = null) + @JvmOverloads + fun copy(inputs: List> = this.inputs, + outputs: List> = this.outputs, + commands: List> = this.commands, + attachments: List = this.attachments, + id: SecureHash = this.id, + notary: Party? = this.notary, + timeWindow: TimeWindow? = this.timeWindow, + privacySalt: PrivacySalt = this.privacySalt, + networkParameters: NetworkParameters? = this.networkParameters + ) = copy(inputs = inputs, + outputs = outputs, + commands = commands, + attachments = attachments, + id = id, + notary = notary, + timeWindow = timeWindow, + privacySalt = privacySalt, + networkParameters = networkParameters, + references = references + ) } diff --git a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt index 07584a0f3e..18936e236a 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -33,6 +33,9 @@ abstract class TraversableTransaction(open val componentGroups: List = deserialiseComponentGroup(ComponentGroupEnum.INPUTS_GROUP, { SerializedBytes(it).deserialize() }) + /** Pointers to reference states, identified by (tx identity hash, output index). */ + override val references: List = deserialiseComponentGroup(ComponentGroupEnum.REFERENCES_GROUP, { SerializedBytes(it).deserialize() }) + override val outputs: List> = deserialiseComponentGroup(ComponentGroupEnum.OUTPUTS_GROUP, { SerializedBytes>(it).deserialize(context = SerializationFactory.defaultFactory.defaultContext.withAttachmentsClassLoader(attachments)) }) /** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */ @@ -59,10 +62,11 @@ abstract class TraversableTransaction(open val componentGroups: List> get() { - val result = mutableListOf(inputs, outputs, commands, attachments) + val result = mutableListOf(inputs, outputs, commands, attachments, references) notary?.let { result += listOf(it) } timeWindow?.let { result += listOf(it) } return result @@ -191,6 +195,7 @@ class FilteredTransaction internal constructor( wtx.attachments.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.ATTACHMENTS_GROUP.ordinal, internalIndex) } if (wtx.notary != null) filter(wtx.notary, ComponentGroupEnum.NOTARY_GROUP.ordinal, 0) if (wtx.timeWindow != null) filter(wtx.timeWindow, ComponentGroupEnum.TIMEWINDOW_GROUP.ordinal, 0) + wtx.references.forEachIndexed { internalIndex, it -> filter(it, ComponentGroupEnum.REFERENCES_GROUP.ordinal, internalIndex) } // It is highlighted that because there is no a signers property in TraversableTransaction, // one cannot specifically filter them in or out. // The above is very important to ensure someone won't filter out the signers component group if at least one diff --git a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt index 86e95881b9..37264e6cbe 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt @@ -43,6 +43,7 @@ data class NotaryChangeWireTransaction( val serializedComponents: List ) : CoreTransaction() { override val inputs: List = serializedComponents[INPUTS.ordinal].deserialize() + override val references: List = emptyList() override val notary: Party = serializedComponents[NOTARY.ordinal].deserialize() /** Identity of the notary service to reassign the states to.*/ val newNotary: Party = serializedComponents[NEW_NOTARY.ordinal].deserialize() @@ -109,6 +110,8 @@ data class NotaryChangeLedgerTransaction( checkEncumbrances() } + override val references: List> = emptyList() + /** We compute the outputs on demand by applying the notary field modification to the inputs */ override val outputs: List> get() = inputs.mapIndexed { pos, (state) -> diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index d5a5dd97cf..1a6400792d 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -82,6 +82,8 @@ data class SignedTransaction(val txBits: SerializedBytes, /** Helper to access the inputs of the contained transaction. */ val inputs: List get() = coreTransaction.inputs + /** Helper to access the unspendable inputs of the contained transaction. */ + val references: List get() = coreTransaction.references /** Helper to access the notary of the contained transaction. */ val notary: Party? get() = coreTransaction.notary diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 80ed0fb27f..8a8212838b 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -45,7 +45,7 @@ import kotlin.collections.ArrayList * [TransactionState] with this notary specified will be generated automatically. */ @DeleteForDJVM -open class TransactionBuilder( +open class TransactionBuilder @JvmOverloads constructor( var notary: Party? = null, var lockId: UUID = (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID(), protected val inputs: MutableList = arrayListOf(), @@ -53,11 +53,11 @@ open class TransactionBuilder( protected val outputs: MutableList> = arrayListOf(), protected val commands: MutableList> = arrayListOf(), protected var window: TimeWindow? = null, - protected var privacySalt: PrivacySalt = PrivacySalt() + protected var privacySalt: PrivacySalt = PrivacySalt(), + protected val references: MutableList = arrayListOf() ) { - constructor(notary: Party) : this(notary, (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID()) - private val inputsWithTransactionState = arrayListOf>() + private val referencesWithTransactionState = arrayListOf>() /** * Creates a copy of the builder. @@ -70,9 +70,11 @@ open class TransactionBuilder( outputs = ArrayList(outputs), commands = ArrayList(commands), window = window, - privacySalt = privacySalt + privacySalt = privacySalt, + references = references ) t.inputsWithTransactionState.addAll(this.inputsWithTransactionState) + t.referencesWithTransactionState.addAll(this.referencesWithTransactionState) return t } @@ -82,6 +84,7 @@ open class TransactionBuilder( for (t in items) { when (t) { is StateAndRef<*> -> addInputState(t) + is ReferencedStateAndRef<*> -> @Suppress("DEPRECATION") addReferenceState(t) // Will remove when feature finalised. is SecureHash -> addAttachment(t) is TransactionState<*> -> addOutputState(t) is StateAndContract -> addOutputState(t.state, t.contract) @@ -123,7 +126,7 @@ open class TransactionBuilder( } return SerializationFactory.defaultFactory.withCurrentContext(serializationContext) { - WireTransaction(WireTransaction.createComponentGroups(inputStates(), resolvedOutputs, commands, attachments + makeContractAttachments(services.cordappProvider), notary, window), privacySalt) + WireTransaction(WireTransaction.createComponentGroups(inputStates(), resolvedOutputs, commands, attachments + makeContractAttachments(services.cordappProvider), notary, window, referenceStates()), privacySalt) } } @@ -137,6 +140,7 @@ open class TransactionBuilder( * TODO - review this logic */ private fun makeContractAttachments(cordappProvider: CordappProvider): List { + // Reference inputs not included as it is not necessary to verify them. return (inputsWithTransactionState + outputs).map { state -> cordappProvider.getContractAttachmentID(state.contract) ?: throw MissingContractAttachments(listOf(state)) @@ -155,42 +159,112 @@ open class TransactionBuilder( toLedgerTransaction(services).verify() } - open fun addInputState(stateAndRef: StateAndRef<*>): TransactionBuilder { + private fun checkNotary(stateAndRef: StateAndRef<*>) { val notary = stateAndRef.state.notary - require(notary == this.notary) { "Input state requires notary \"$notary\" which does not match the transaction notary \"${this.notary}\"." } + require(notary == this.notary) { + "Input state requires notary \"$notary\" which does not match the transaction notary \"${this.notary}\"." + } + } + + // This check is performed here as well as in BaseTransaction. + private fun checkForInputsAndReferencesOverlap() { + val intersection = inputs intersect references + require(intersection.isEmpty()) { + "A StateRef cannot be both an input and a reference input in the same transaction." + } + } + + private fun checkReferencesUseSameNotary() = referencesWithTransactionState.map { it.notary }.toSet().size == 1 + + /** + * Adds a reference input [StateRef] to the transaction. + * + * This feature was added in version 4 of Corda, so will throw an exception for any Corda networks with a minimum + * platform version less than 4. + * + * @throws UncheckedVersionException + */ + @Deprecated(message = "Feature not yet released. Pending stabilisation.") + open fun addReferenceState(referencedStateAndRef: ReferencedStateAndRef<*>): TransactionBuilder { + val stateAndRef = referencedStateAndRef.stateAndRef + referencesWithTransactionState.add(stateAndRef.state) + + // It is likely the case that users of reference states do not have permission to change the notary assigned + // to a reference state. Even if users _did_ have this permission the result would likely be a bunch of + // notary change races. As such, if a reference state is added to a transaction which is assigned to a + // different notary to the input and output states then all those inputs and outputs must be moved to the + // notary which the reference state uses. + // + // If two or more reference states assigned to different notaries are added to a transaction then it follows + // that this transaction likely _cannot_ be committed to the ledger as it unlikely that the party using the + // reference state can change the assigned notary for one of the reference states. + // + // As such, if reference states assigned to multiple different notaries are added to a transaction builder + // then the check below will fail. + check(checkReferencesUseSameNotary()) { + "Transactions with reference states using multiple different notaries are currently unsupported." + } + + checkNotary(stateAndRef) + references.add(stateAndRef.ref) + checkForInputsAndReferencesOverlap() + return this + } + + /** Adds an input [StateRef] to the transaction. */ + open fun addInputState(stateAndRef: StateAndRef<*>): TransactionBuilder { + checkNotary(stateAndRef) inputs.add(stateAndRef.ref) inputsWithTransactionState.add(stateAndRef.state) return this } + /** Adds an attachment with the specified hash to the TransactionBuilder. */ fun addAttachment(attachmentId: SecureHash): TransactionBuilder { attachments.add(attachmentId) return this } + /** Adds an output state to the transaction. */ fun addOutputState(state: TransactionState<*>): TransactionBuilder { outputs.add(state) return this } + /** Adds an output state, with associated contract code (and constraints), and notary, to the transaction. */ @JvmOverloads - fun addOutputState(state: ContractState, contract: ContractClassName, notary: Party, encumbrance: Int? = null, constraint: AttachmentConstraint = AutomaticHashConstraint): TransactionBuilder { + fun addOutputState( + state: ContractState, + contract: ContractClassName, + notary: Party, encumbrance: Int? = null, + constraint: AttachmentConstraint = AutomaticHashConstraint + ): TransactionBuilder { return addOutputState(TransactionState(state, contract, notary, encumbrance, constraint)) } /** A default notary must be specified during builder construction to use this method */ @JvmOverloads - fun addOutputState(state: ContractState, contract: ContractClassName, constraint: AttachmentConstraint = AutomaticHashConstraint): TransactionBuilder { - checkNotNull(notary) { "Need to specify a notary for the state, or set a default one on TransactionBuilder initialisation" } + fun addOutputState( + state: ContractState, contract: ContractClassName, + constraint: AttachmentConstraint = AutomaticHashConstraint + ): TransactionBuilder { + checkNotNull(notary) { + "Need to specify a notary for the state, or set a default one on TransactionBuilder initialisation" + } addOutputState(state, contract, notary!!, constraint = constraint) return this } + /** Adds a [Command] to the transaction. */ fun addCommand(arg: Command<*>): TransactionBuilder { commands.add(arg) return this } + /** + * Adds a [Command] to the transaction, specified by the encapsulated [CommandData] object and required list of + * signing [PublicKey]s. + */ fun addCommand(data: CommandData, vararg keys: PublicKey) = addCommand(Command(data, listOf(*keys))) fun addCommand(data: CommandData, keys: List) = addCommand(Command(data, keys)) @@ -219,11 +293,19 @@ open class TransactionBuilder( return this } - // Accessors that yield immutable snapshots. + /** Returns an immutable list of input [StateRefs]. */ fun inputStates(): List = ArrayList(inputs) + /** Returns an immutable list of reference input [StateRefs]. */ + fun referenceStates(): List = ArrayList(references) + + /** Returns an immutable list of attachment hashes. */ fun attachments(): List = ArrayList(attachments) + + /** Returns an immutable list of output [TransactionState]s. */ fun outputStates(): List> = ArrayList(outputs) + + /** Returns an immutable list of [Command]s. */ fun commands(): List> = ArrayList(commands) /** diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index e081e42aac..fad22a95fd 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -58,15 +58,16 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr @DeleteForDJVM constructor(componentGroups: List) : this(componentGroups, PrivacySalt()) - @Deprecated("Required only for backwards compatibility purposes.", ReplaceWith("WireTransaction(val componentGroups: List, override val privacySalt: PrivacySalt)"), DeprecationLevel.WARNING) + @Deprecated("Required only in some unit-tests and for backwards compatibility purposes.", ReplaceWith("WireTransaction(val componentGroups: List, override val privacySalt: PrivacySalt)"), DeprecationLevel.WARNING) @DeleteForDJVM - constructor(inputs: List, - attachments: List, - outputs: List>, - commands: List>, - notary: Party?, - timeWindow: TimeWindow?, - privacySalt: PrivacySalt = PrivacySalt() + @JvmOverloads constructor( + inputs: List, + attachments: List, + outputs: List>, + commands: List>, + notary: Party?, + timeWindow: TimeWindow?, + privacySalt: PrivacySalt = PrivacySalt() ) : this(createComponentGroups(inputs, outputs, commands, attachments, notary, timeWindow), privacySalt) init { @@ -86,7 +87,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr get() { val commandKeys = commands.flatMap { it.signers }.toSet() // TODO: prevent notary field from being set if there are no inputs and no time-window. - return if (notary != null && (inputs.isNotEmpty() || timeWindow != null)) { + return if (notary != null && (inputs.isNotEmpty() || references.isNotEmpty() || timeWindow != null)) { commandKeys + notary.owningKey } else { commandKeys @@ -143,8 +144,11 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr val resolvedInputs = inputs.map { ref -> resolveStateRef(ref)?.let { StateAndRef(it, ref) } ?: throw TransactionResolutionException(ref.txhash) } + val resolvedReferences = references.map { ref -> + resolveStateRef(ref)?.let { StateAndRef(it, ref) } ?: throw TransactionResolutionException(ref.txhash) + } val attachments = attachments.map { resolveAttachment(it) ?: throw AttachmentResolutionException(it) } - val ltx = LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, privacySalt, networkParameters) + val ltx = LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, privacySalt, networkParameters, resolvedReferences) checkTransactionSize(ltx, networkParameters?.maxTransactionSize ?: 10485760) return ltx } @@ -160,6 +164,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr // Check attachments size first as they are most likely to go over the limit. With ContractAttachment instances // it's likely that the same underlying Attachment CorDapp will occur more than once so we dedup on the attachment id. ltx.attachments.distinctBy { it.id }.forEach { minus(it.size) } + minus(ltx.references.serialize().size) minus(ltx.inputs.serialize().size) minus(ltx.commands.serialize().size) minus(ltx.outputs.serialize().size) @@ -249,15 +254,18 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr * Creating list of [ComponentGroup] used in one of the constructors of [WireTransaction] required * for backwards compatibility purposes. */ + @JvmOverloads @CordaInternal fun createComponentGroups(inputs: List, outputs: List>, commands: List>, attachments: List, notary: Party?, - timeWindow: TimeWindow?): List { + timeWindow: TimeWindow?, + references: List = emptyList()): List { val componentGroupMap: MutableList = mutableListOf() if (inputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(INPUTS_GROUP.ordinal, inputs.map { it.serialize() })) + if (references.isNotEmpty()) componentGroupMap.add(ComponentGroup(REFERENCES_GROUP.ordinal, references.map { it.serialize() })) if (outputs.isNotEmpty()) componentGroupMap.add(ComponentGroup(OUTPUTS_GROUP.ordinal, outputs.map { it.serialize() })) // Adding commandData only to the commands group. Signers are added in their own group. if (commands.isNotEmpty()) componentGroupMap.add(ComponentGroup(COMMANDS_GROUP.ordinal, commands.map { it.value.serialize() })) @@ -275,6 +283,7 @@ class WireTransaction(componentGroups: List, val privacySalt: Pr override fun toString(): String { val buf = StringBuilder() buf.appendln("Transaction:") + for (reference in references) buf.appendln("${Emoji.rightArrow}REFS: $reference") for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input") for ((data) in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $data") for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command") diff --git a/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt new file mode 100644 index 0000000000..fc9c26b230 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt @@ -0,0 +1,164 @@ +package net.corda.core.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.* +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.node.StatesToRecord +import net.corda.core.node.services.queryBy +import net.corda.core.node.services.vault.QueryCriteria +import net.corda.core.transactions.LedgerTransaction +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.getOrThrow +import net.corda.node.VersionInfo +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.contracts.DummyContract +import net.corda.testing.contracts.DummyState +import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.InternalMockNodeParameters +import net.corda.testing.node.internal.cordappsForPackages +import net.corda.testing.node.internal.startFlow +import org.junit.After +import org.junit.Test +import kotlin.test.assertEquals + + +// A dummy reference state contract. +internal class RefState : Contract { + companion object { + val CONTRACT_ID = "net.corda.core.flows.RefState" + } + + override fun verify(tx: LedgerTransaction) = Unit + data class State(val owner: Party, val version: Int = 0, override val linearId: UniqueIdentifier = UniqueIdentifier()) : LinearState { + override val participants: List get() = listOf(owner) + fun update() = copy(version = version + 1) + } + + class Create : CommandData + class Update : CommandData +} + +// A flow to create a reference state. +internal class CreateRefState : FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + val notary = serviceHub.networkMapCache.notaryIdentities.first() + return subFlow(FinalityFlow( + transaction = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply { + addOutputState(RefState.State(ourIdentity), RefState.CONTRACT_ID) + addCommand(RefState.Create(), listOf(ourIdentity.owningKey)) + }) + )) + } +} + +// A flow to update a specific reference state. +internal class UpdateRefState(val stateAndRef: StateAndRef) : FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + val notary = serviceHub.networkMapCache.notaryIdentities.first() + return subFlow(FinalityFlow( + transaction = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply { + addInputState(stateAndRef) + addOutputState((stateAndRef.state.data as RefState.State).update(), RefState.CONTRACT_ID) + addCommand(RefState.Update(), listOf(ourIdentity.owningKey)) + }) + )) + } +} + +// A set of flows to share a stateref with all other nodes in the mock network. +internal object ShareRefState { + @InitiatingFlow + class Initiator(val stateAndRef: StateAndRef) : FlowLogic() { + @Suspendable + override fun call() { + val sessions = serviceHub.networkMapCache.allNodes.flatMap { it.legalIdentities }.map { initiateFlow(it) } + val transactionId = stateAndRef.ref.txhash + val transaction = serviceHub.validatedTransactions.getTransaction(transactionId) + ?: throw FlowException("Cannot find $transactionId.") + sessions.forEach { subFlow(SendTransactionFlow(it, transaction)) } + } + } + + @InitiatedBy(ShareRefState.Initiator::class) + class Responder(val otherSession: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + logger.info("Receiving dependencies.") + subFlow(ReceiveTransactionFlow( + otherSideSession = otherSession, + checkSufficientSignatures = true, + statesToRecord = StatesToRecord.ALL_VISIBLE + )) + } + } +} + +// A flow to use a reference state in another transaction. +internal class UseRefState(val linearId: UniqueIdentifier) : FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + val notary = serviceHub.networkMapCache.notaryIdentities.first() + val query = QueryCriteria.LinearStateQueryCriteria(linearId = listOf(linearId)) + val referenceState = serviceHub.vaultService.queryBy(query).states.single() + return subFlow(FinalityFlow( + transaction = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply { + @Suppress("DEPRECATION") // To be removed when feature is finalised. + addReferenceState(referenceState.referenced()) + addOutputState(DummyState(), DummyContract.PROGRAM_ID) + addCommand(DummyContract.Commands.Create(), listOf(ourIdentity.owningKey)) + }) + )) + } +} + + +class WithReferencedStatesFlowTests { + companion object { + @JvmStatic + private val mockNet = InternalMockNetwork( + cordappsForAllNodes = cordappsForPackages("net.corda.core.flows", "net.corda.testing.contracts"), + threadPerNode = true, + networkParameters = testNetworkParameters(minimumPlatformVersion = 4) + ) + } + + private val nodes = (0..1).map { + mockNet.createNode( + parameters = InternalMockNodeParameters(version = VersionInfo(4, "Blah", "Blah", "Blah")) + ) + } + + @After + fun stop() { + mockNet.stopNodes() + } + + @Test + fun test() { + // 1. Create reference state. + val newRefTx = nodes[0].services.startFlow(CreateRefState()).resultFuture.getOrThrow() + val newRefState = newRefTx.tx.outRefsOfType().single() + + // 2. Share it with others. + nodes[0].services.startFlow(ShareRefState.Initiator(newRefState)).resultFuture.getOrThrow() + + // 3. Update the reference state but don't share the update. + val updatedRefTx = nodes[0].services.startFlow(UpdateRefState(newRefState)).resultFuture.getOrThrow() + val updatedRefState = updatedRefTx.tx.outRefsOfType().single() + + // 4. Try to use the old reference state. This will throw a NotaryException. + val useRefTx = nodes[1].services.startFlow(WithReferencedStatesFlow(UseRefState(newRefState.state.data.linearId))).resultFuture + + // 5. Share the update reference state. + nodes[0].services.startFlow(ShareRefState.Initiator(updatedRefState)).resultFuture.getOrThrow() + + // 6. Check that we have a valid signed transaction with the updated reference state. + val result = useRefTx.getOrThrow() + assertEquals(updatedRefState.ref, result.tx.references.single()) + } + +} \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/internal/TopologicalSortTest.kt b/core/src/test/kotlin/net/corda/core/internal/TopologicalSortTest.kt index 6237a1a019..fe427339e3 100644 --- a/core/src/test/kotlin/net/corda/core/internal/TopologicalSortTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/TopologicalSortTest.kt @@ -24,8 +24,9 @@ class TopologicalSortTest { class DummyTransaction constructor( override val id: SecureHash, override val inputs: List, - @Suppress("CanBeParameter") private val numberOfOutputs: Int, - override val notary: Party + @Suppress("CanBeParameter") val numberOfOutputs: Int, + override val notary: Party, + override val references: List = emptyList() ) : CoreTransaction() { override val outputs: List> = (1..numberOfOutputs).map { TransactionState(DummyState(), "", notary) diff --git a/core/src/test/kotlin/net/corda/core/transactions/ReferenceInputStateTests.kt b/core/src/test/kotlin/net/corda/core/transactions/ReferenceInputStateTests.kt new file mode 100644 index 0000000000..8b09b574ca --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/transactions/ReferenceInputStateTests.kt @@ -0,0 +1,196 @@ +package net.corda.core.transactions + +import com.nhaarman.mockito_kotlin.doReturn +import com.nhaarman.mockito_kotlin.whenever +import net.corda.core.contracts.* +import net.corda.core.contracts.Requirements.using +import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.CordaX500Name +import net.corda.core.identity.Party +import net.corda.finance.DOLLARS +import net.corda.finance.`issued by` +import net.corda.finance.contracts.asset.Cash +import net.corda.node.services.api.IdentityServiceInternal +import net.corda.testing.common.internal.testNetworkParameters +import net.corda.testing.core.DUMMY_NOTARY_NAME +import net.corda.testing.core.SerializationEnvironmentRule +import net.corda.testing.core.TestIdentity +import net.corda.testing.internal.rigorousMock +import net.corda.testing.node.MockServices +import net.corda.testing.node.ledger +import org.junit.Rule +import org.junit.Test +import kotlin.test.assertFailsWith + +val CONTRACT_ID = "net.corda.core.transactions.ReferenceStateTests\$ExampleContract" + +class ReferenceStateTests { + private companion object { + val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party + val ISSUER = TestIdentity(CordaX500Name("ISSUER", "London", "GB")) + val ALICE = TestIdentity(CordaX500Name("ALICE", "London", "GB")) + val ALICE_PARTY get() = ALICE.party + val ALICE_PUBKEY get() = ALICE.publicKey + val BOB = TestIdentity(CordaX500Name("BOB", "London", "GB")) + val BOB_PARTY get() = BOB.party + val BOB_PUBKEY get() = BOB.publicKey + } + + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + val defaultIssuer = ISSUER.ref(1) + val bobCash = Cash.State(amount = 1000.DOLLARS `issued by` defaultIssuer, owner = BOB_PARTY) + private val ledgerServices = MockServices( + cordappPackages = listOf("net.corda.core.transactions", "net.corda.finance.contracts.asset"), + initialIdentity = ALICE, + identityService = rigorousMock().also { + doReturn(ALICE_PARTY).whenever(it).partyFromKey(ALICE_PUBKEY) + doReturn(BOB_PARTY).whenever(it).partyFromKey(BOB_PUBKEY) + }, + networkParameters = testNetworkParameters(minimumPlatformVersion = 4) + + ) + + // This state has only been created to serve reference data so it cannot ever be used as an input or + // output when it is being referred to. However, we might want all states to be referable, so this + // check might not be present in other contracts, like Cash, for example. Cash might have a command + // called "Share" that allows a party to prove to another that they own over a certain amount of cash. + // As such, cash can be added to the references list with a "Share" command. + data class ExampleState(val creator: Party, val data: String) : ContractState { + override val participants: List get() = listOf(creator) + } + + class ExampleContract : Contract { + interface Commands : CommandData + class Create : Commands + class Update : Commands + + override fun verify(tx: LedgerTransaction) { + val command = tx.commands.requireSingleCommand() + when (command.value) { + is Create -> requireThat { + "Must have no inputs" using (tx.inputs.isEmpty()) + "Must have only one output" using (tx.outputs.size == 1) + val output = tx.outputsOfType().single() + val signedByCreator = command.signers.single() == output.participants.single().owningKey + "Must be signed by creator" using signedByCreator + } + is Update -> { + "Must have no inputs" using (tx.inputs.size == 1) + "Must have only one output" using (tx.outputs.size == 1) + val input = tx.inputsOfType().single() + val output = tx.outputsOfType().single() + "Must update the data" using (input.data != output.data) + val signedByCreator = command.signers.single() == output.participants.single().owningKey + "Must be signed by creator" using signedByCreator + } + } + } + } + + @Test + fun `create a reference state then refer to it multiple times`() { + ledgerServices.ledger(DUMMY_NOTARY) { + // Create a reference state. The reference state is created in the normal way. A transaction with one + // or more outputs. It makes sense to create them one at a time, so the creator can have fine grained + // control over who sees what. + transaction { + output(CONTRACT_ID, "REF DATA", ExampleState(ALICE_PARTY, "HELLO CORDA")) + command(ALICE_PUBKEY, ExampleContract.Create()) + verifies() + } + // Somewhere down the line, Bob obtains the ExampleState and now refers to it as a reference state. As such, + // it is added to the references list. + transaction { + reference("REF DATA") + input(Cash.PROGRAM_ID, bobCash) + output(Cash.PROGRAM_ID, "ALICE CASH", bobCash.withNewOwner(ALICE_PARTY).ownableState) + command(BOB_PUBKEY, Cash.Commands.Move()) + verifies() + } + // Alice can use it too. + transaction { + reference("REF DATA") + input("ALICE CASH") + output(Cash.PROGRAM_ID, "BOB CASH 2", bobCash.withNewOwner(BOB_PARTY).ownableState) + command(ALICE_PUBKEY, Cash.Commands.Move()) + verifies() + } + // Bob can use it again. + transaction { + reference("REF DATA") + input("BOB CASH 2") + output(Cash.PROGRAM_ID, bobCash.withNewOwner(ALICE_PARTY).ownableState) + command(BOB_PUBKEY, Cash.Commands.Move()) + verifies() + } + } + } + + @Test + fun `Non-creator node cannot spend spend a reference state`() { + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + output(CONTRACT_ID, "REF DATA", ExampleState(ALICE_PARTY, "HELLO CORDA")) + command(ALICE_PUBKEY, ExampleContract.Create()) + verifies() + } + // Try to spend an unspendable input by accident. Opps! This should fail as per the contract above. + transaction { + input("REF DATA") + input(Cash.PROGRAM_ID, bobCash) + output(Cash.PROGRAM_ID, bobCash.withNewOwner(ALICE_PARTY).ownableState) + command(BOB_PUBKEY, Cash.Commands.Move()) + fails() + } + } + } + + @Test + fun `Can't use old reference states`() { + val refData = ExampleState(ALICE_PARTY, "HELLO CORDA") + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + output(CONTRACT_ID, "REF DATA", refData) + command(ALICE_PUBKEY, ExampleContract.Create()) + verifies() + } + // Refer to it. All OK. + transaction { + reference("REF DATA") + input(Cash.PROGRAM_ID, bobCash) + output(Cash.PROGRAM_ID, "ALICE CASH", bobCash.withNewOwner(ALICE_PARTY).ownableState) + command(BOB_PUBKEY, Cash.Commands.Move()) + verifies() + } + // Update it. + transaction { + input("REF DATA") + command(ALICE_PUBKEY, ExampleContract.Update()) + output(Cash.PROGRAM_ID, "UPDATED REF DATA", "REF DATA".output().copy(data = "NEW STUFF!")) + verifies() + } + // Try to use the old one. + transaction { + reference("REF DATA") + input("ALICE CASH") + output(Cash.PROGRAM_ID, bobCash.withNewOwner(BOB_PARTY).ownableState) + command(ALICE_PUBKEY, Cash.Commands.Move()) + verifies() + } + fails() // "double spend" of ExampleState!! Alice updated it in the 3rd transaction. + } + } + + @Test + fun `state ref cannot be a reference input and regular input in the same transaction`() { + val state = ExampleState(ALICE_PARTY, "HELLO CORDA") + val stateAndRef = StateAndRef(TransactionState(state, CONTRACT_ID, DUMMY_NOTARY), StateRef(SecureHash.zeroHash, 0)) + assertFailsWith(IllegalArgumentException::class, "A StateRef cannot be both an input and a reference input in the same transaction.") { + @Suppress("DEPRECATION") // To be removed when feature is finalised. + TransactionBuilder(notary = DUMMY_NOTARY).addInputState(stateAndRef).addReferenceState(stateAndRef.referenced()) + } + } +} \ No newline at end of file diff --git a/docs/source/api-transactions.rst b/docs/source/api-transactions.rst index 888aed09e7..0dfd5863a9 100644 --- a/docs/source/api-transactions.rst +++ b/docs/source/api-transactions.rst @@ -36,6 +36,7 @@ A transaction consists of six types of components: * 0+ input states * 0+ output states + * 0+ reference input states * 1+ commands * 0+ attachments @@ -90,6 +91,53 @@ The ``StateRef`` links an input state back to the transaction that created it. T "chains" linking each input back to an original issuance transaction. This allows nodes verifying the transaction to "walk the chain" and verify that each input was generated through a valid sequence of transactions. +Reference input states +~~~~~~~~~~~~~~~~~~~~~~ + +A reference input state is a ``ContractState`` which can be referred to in a transaction by the contracts of input and +output states but whose contract is not executed as part of the transaction verification process. Furthermore, +reference states are not consumed when the transaction is committed to the ledger but they are checked for +"current-ness". In other words, the contract logic isn't run for the referencing transaction only. It's still a normal +state when it occurs in an input or output position. + +Reference data states enable many parties to "reuse" the same state in their transactions as reference data whilst +still allowing the reference data state owner the capability to update the state. + +A reference input state is added to a transaction as a ``ReferencedStateAndRef``. A ``ReferencedStateAndRef`` can be +obtained from a ``StateAndRef`` by calling the ``StateAndRef.referenced()`` method which returns a +``ReferencedStateAndRef``. + +.. warning:: Reference states are only available on Corda networks with a minimum platform version >= 4. + +.. container:: codeset + + .. literalinclude:: ../../docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt +:language: kotlin + :start-after: DOCSTART 55 + :end-before: DOCEND 55 + :dedent: 8 + + .. literalinclude:: ../../docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java +:language: java + :start-after: DOCSTART 55 + :end-before: DOCEND 55 + :dedent: 12 + +**Known limitations:** + +*Notary change:* It is likely the case that users of reference states do not have permission to change the notary assigned +to a reference state. Even if users *did* have this permission the result would likely be a bunch of +notary change races. As such, if a reference state is added to a transaction which is assigned to a +different notary to the input and output states then all those inputs and outputs must be moved to the +notary which the reference state uses. + +If two or more reference states assigned to different notaries are added to a transaction then it follows +that this transaction likely *cannot* be committed to the ledger as it unlikely that the party using the +reference state can change the assigned notary for one of the reference states. + +As such, if reference states assigned to multiple different notaries are added to a transaction builder +then the check below will fail. + Output states ^^^^^^^^^^^^^ Since a transaction's output states do not exist until the transaction is committed, they cannot be referenced as the @@ -298,6 +346,7 @@ We can add components to the builder using the ``TransactionBuilder.withItems`` ``withItems`` takes a ``vararg`` of objects and adds them to the builder based on their type: * ``StateAndRef`` objects are added as input states +* ``ReferencedStateAndRef`` objects are added as reference input states * ``TransactionState`` and ``StateAndContract`` objects are added as output states * Both ``TransactionState`` and ``StateAndContract`` are wrappers around a ``ContractState`` output that link the diff --git a/docs/source/blob-inspector.rst b/docs/source/blob-inspector.rst index 0814b2ef88..f0948ee0dc 100644 --- a/docs/source/blob-inspector.rst +++ b/docs/source/blob-inspector.rst @@ -2,9 +2,9 @@ Blob Inspector ============== There are many benefits to having a custom binary serialisation format (see :doc:`serialization` for details) but one -disadvantage is the inability to view the contents in a human-friendly manner. The blob inspector tool alleviates this issue -by allowing the contents of a binary blob file (or URL end-point) to be output in either YAML or JSON. It uses -``JacksonSupport`` to do this (see :doc:`json`). +disadvantage is the inability to view the contents in a human-friendly manner. The Corda Blob Inspector tool alleviates +this issue by allowing the contents of a binary blob file (or URL end-point) to be output in either YAML or JSON. It +uses ``JacksonSupport`` to do this (see :doc:`json`). The tool is distributed as part of |release| in the form of runnable JAR "|jar_name|". @@ -58,8 +58,9 @@ Example Here's what a node-info file from the node's data directory may look like: -**-\\-format=YAML** -:: +* YAML: + +.. sourcecode:: none net.corda.nodeapi.internal.SignedNodeInfo --- @@ -76,8 +77,9 @@ Here's what a node-info file from the node's data directory may look like: - !!binary |- VFRy4frbgRDbCpK1Vo88PyUoj01vbRnMR3ROR2abTFk7yJ14901aeScX/CiEP+CDGiMRsdw01cXt\nhKSobAY7Dw== -**-\\-format=JSON** -:: +* JSON: + +.. sourcecode:: none net.corda.nodeapi.internal.SignedNodeInfo { diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index cb36effe06..52c093174b 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -189,6 +189,12 @@ Unreleased * Upgraded Artemis to v2.6.2. +* Introduced the concept of "reference input states". A reference input state is a ``ContractState`` which can be referred + to in a transaction by the contracts of input and output states but whose contract is not executed as part of the + transaction verification process and is not consumed when the transaction is committed to the ledger but is checked + for "current-ness". In other words, the contract logic isn't run for the referencing transaction only. It's still a + normal state when it occurs in an input or output position. + .. _changelog_v3.1: Version 3.1 diff --git a/docs/source/design/reference-states/design.md b/docs/source/design/reference-states/design.md new file mode 100644 index 0000000000..56e012499e --- /dev/null +++ b/docs/source/design/reference-states/design.md @@ -0,0 +1,239 @@ +![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png) + +# Design + +DOCUMENT MANAGEMENT +--- + +Design documents should follow the standard GitHub version management and pull request (PR) review workflow mechanism. + +## Document Control + +| Title | | +| -------------------- | ---------------------------------------- | +| Date | 27 March 2018 | +| Author | Roger Willis | +| Distribution | Matthew Nesbit, Rick Parker | +| Corda target version | open source and enterprise | +| JIRA reference | No JIRA's raised. | + +## Approvals + +#### Document Sign-off + +| Author | | +| ----------------- | ---------------------------------------- | +| Reviewer(s) | (GitHub PR reviewers) | +| Final approver(s) | (GitHub PR approver(s) from Design Approval Board) | + +#### Design Decisions + +There's only really one way to do this that satisfies our requirements - add a new input `StateAndRef` component group to the transaction classes. Other possible solutions are discussed below and why they are inappropriate. + +## Document History + +* [Version 1](https://github.com/corda/enterprise/blob/779aaefa5c09a6a28191496dd45252b6e207b7f7/docs/source/design/reference-states/design.md) (Received comments from Richard Brown and Mark Oldfield). +* [Version 2](https://github.com/corda/enterprise/blob/a87f1dcb22ba15081b0da92ba1501b6b81ae2baf/docs/source/design/reference-states/design.md) (Version presented to the DRB). + +HIGH LEVEL DESIGN +--- + +## Overview + +See a prototype implementation here: https://github.com/corda/corda/pull/2889 + +There is an increasing need for Corda to support use-cases which require reference data which is issued and updated by specific parties, but available for use, by reference, in transactions built by other parties. + +Why is this type of reference data required? + +1. A key benefit of blockchain systems is that everybody is sure they see the same as their counterpart - and for this to work in situations where accurate processing depends on reference data requires everybody to be operating on the same reference data. +2. This, in turn, requires any given piece of reference data to be uniquely identifiable and, requires that any given transaction must be certain to be operating on the most current version of that reference data. +3. In cases where the latter condition applies, only the notary can attest to this fact and this, in turn, means the reference data must be in the form of an unconsumed state. + +This document outlines the approach for adding support for this type of reference data to the Corda transaction model via a new approach called "reference input states". + +## Background + +Firstly, it is worth considering the types of reference data on Corda how it is distributed: + +1. **Rarely changing universal reference data.** Such as currency codes and holiday calendars. This type of data can be added to transactions as attachments and referenced within contracts, if required. This data would only change based upon the decision of an International standards body, for example, therefore it is not critical to check the data is current each time it is used. +2. **Constantly changing reference data.** Typically, this type of data must be collected and aggregated by a central party. Oracles can be used as a central source of truth for this type of constantly changing data. There are multiple examples of making transaction validity contingent on data provided by Oracles (IRS demo and SIMM demo). The Oracle asserts the data was valid at the time it was provided. +3. **Periodically changing subjective reference data.** Reference data provided by entities such as bond issuers where the data changes frequently enough to warrant users of the data check it is current. + +At present, periodically changing subjective data can only be provided via: + +* Oracles, +* Attachments, +* Regular contract states, or alternatively, +* kept off-ledger entirely + +However, neither of these solutions are optimal for reasons discussed in later sections of this design document. + +As such, this design document introduces the concept of a "reference input state" which is a better way to serve "periodically changing subjective reference data" on Corda. + +*What is a "reference input state"?* + +A reference input state is a `ContractState` which can be referred to in a transaction by the contracts of input and output states but whose contract is not executed as part of the transaction verification process and is not consumed when the transaction is committed to the ledger but _is_ checked for "current-ness". In other words, the contract logic isn't run for the referencing transaction only. It's still a normal state when it occurs in an input or output position. + +*What will reference input states enable?* + +Reference data states will enable many parties to "reuse" the same state in their transactions as reference data whilst still allowing the reference data state owner the capability to update the state. When data distribution groups are available then reference state owners will be able to distribute updates to subscribers more easily. Currently, distribution would have to be performed manually. + +*Roughly, how are reference input states implemented?* + +Reference input states can be added to Corda by adding a new transaction component group that allows developers to add reference data `ContractState`s that are not consumed when the transaction is committed to the ledger. This eliminates the problems created by long chains of provenance, contention, and allows developers to use any `ContractState` for reference data. The feature should allow developers to add _any_ `ContractState` available in their vault, even if they are not a `participant` whilst nevertheless providing a guarantee that the state being used is the most recent version of that piece of information. + +## Scope + +Goals + +* Add the capability to Corda transactions to support reference states + +Non-goals (eg. out of scope) + +* Data distribution groups are required to realise the full potential of reference data states. This design document does not discuss data distribution groups. + +## Timeline + +This work should be ready by the release of Corda V4. There is a prototype which is currently good enough for one of the firm's most critical projects, but more work is required: + +* to assess the impact of this change +* write tests +* write documentation + +## Requirements + +1. Reference states can be any `ContractState` created by one or more `Party`s and subsequently updated by those `Party`s. E.g. `Cash`, `CompanyData`, `InterestRateSwap`, `FxRate`. Reference states can be `OwnableState`s, but it's more likely they will be `LinearState`s. +2. Any `Party` with a `StateRef` for a reference state should be able to add it to a transaction to be used as a reference, even if they are not a `participant` of the reference state. +3. The contract code for reference states should not be executed. However, reference data states can be referred to by the contracts of `ContractState`s in the input and output lists. +4. `ContractStates` should not be consumed when used as reference data. +5. Reference data must be current, therefore when reference data states are used in a transaction, notaries should check that they have not been consumed before. +6. To ensure determinism of the contract verification process, reference data states must be in scope for the purposes of transaction resolution. This is because whilst users of the reference data are not consuming the state, they must be sure that the series of transactions that created and evolved the state were executed validly. + +**Use-cases:** + +The canonical use-case for reference states: *KYC* + +* KYC data can be distributed as reference data states. +* KYC data states can only updatable by the data owner. +* Usable by any party - transaction verification can be conditional on this KYC/reference data. +* Notary ensures the data is current. + +Collateral reporting: + +* Imagine a bank needs to provide evidence to another party (like a regulator) that they hold certain states, such as cash and collateral, for liquidity reporting purposes +* The regulator holds a liquidity reporting state that maintains a record of past collateral reports and automates the handling of current reports using some contract code +* To update the liquidity reporting state, the regulator needs to include the bank’s cash/collateral states in a transaction – the contract code checks available collateral vs requirements. By doing this, the cash/collateral states would be consumed, which is not desirable +* Instead, what if those cash/collateral states could be referenced in a transaction but not consumed? And at the same time, the notary still checks to see if the cash/collateral states are current, or not (i.e. does the bank still own them) + +Other uses: + +* Distributing reference data for financial instruments. E.g. Bond issuance details created, updated and distributed by the bond issuer rather than a third party. +* Account level data included in cash payment transactions. + +## Design Decisions + +There are various other ways to implement reference data on Corda, discussed below: + +**Regular contract states** + +Currently, the transaction model is too cumbersome to support reference data as unconsumed states for the following reasons: + +* Contract verification is required for the `ContractState`s used as reference data. This limits the use of states, such as `Cash` as reference data (unless a special "reference" command is added which allows a "NOOP" state transaction to assert no that changes were made.) +* As such, whenever an input state reference is added to a transaction as reference data, an output state must be added, otherwise the state will be extinguished. This results in long chains of unnecessarily duplicated data. +* Long chains of provenance result in confidentiality breaches as down-stream users of the reference data state see all the prior uses of the reference data in the chain of provenance. This is an important point: it means that two parties, who have no business relationship and care little about each other's transactions nevertheless find themselves intimately bound: should one of them rely on a piece of common reference data in a transaction, the other one will not only need to be informed but will need to be furnished with a copy of the transaction. +* Reference data states will likely be used by many parties so they will be come highly contended. Parties will "race" to use the reference data. The latest copy must be continually distributed to all that require it. + +**Attachments** + +Of course, attachments can be used to store and share reference data. This approach does solve the contention issue around reference data as regular contract states. However, attachments don't allow users to ascertain whether they are working on the most recent copy of the data. Given that it's crucial to know whether reference data is current, attachments cannot provide a workable solution here. + +The other issue with attachments is that they do not give an intrinsic "format" to data, like state objects do. This makes working with attachments much harder as their contents are effectively bespoke. Whilst a data format tool could be written, it's more convenient to work with state objects. + +**Oracles** + +Whilst Oracles could provide a solution for periodically changing reference data, they introduce unnecessary centralisation and are onerous to implement for each class of reference data. Oracles don't feel like an optimal solution here. + +**Keeping reference data off-ledger** + +It makes sense to push as much verification as possible into the contract code, otherwise why bother having it? Performing verification inside flows is generally not a good idea as the flows can be re-written by malicious developers. In almost all cases, it is much more difficult to change the contract code. If transaction verification can be conditional on reference data included in a transaction, as a state, then the result is a more robust and secure ledger (and audit trail). + +## Target Solution + +Changes required: + +1. Add a `references` property of type `List` and `List` (for `FullTransaction`s) to all the transaction types. +2. Add a `REFERENCE_STATES` component group. +3. Amend the notary flows to check that reference states are current (but do not consume them) +4. Add a `ReferencedStateAndRef` class that encapsulates a `StateAndRef`, this is so `TransactionBuilder.withItems` can delineate between `StateAndRef`s and state references. +5. Add a `StateAndRef.referenced` method which wraps a `StateAndRef` in a `ReferencedStateAndRef`. +6. Add helper methods to `LedgerTransaction` to get `references` by type, etc. +7. Add a check to the transaction classes that asserts all references and inputs are on the same notary. +8. Add a method to `TransactionBuilder` to add a reference state. +9. Update the transaction resolution flow to resolve references. +10. Update the transaction and ledger DSLs to support references. +11. No changes are required to be made to contract upgrade or notary change transactions. + +Implications: + +**Versioning** + +This can be done in a backwards compatible way. However, a minimum platform version must be mandated. Nodes running on an older version of Corda will not be able to verify transactions which include references. Indeed, contracts which refer to `references` will fail at run-time for older nodes. + +**Privacy** + +Reference states will be visible to all that possess a chain of provenance including them. There are potential implications from a data protection perspective here. Creators of reference data must be careful **not** to include sensitive personal data. + +Outstanding issues: + +**Oracle choice** + +If the party building a transaction is using a reference state which they are not the owner of, they must move their states to the reference state's notary. If two or more reference states with different notaries are used, then the transaction cannot be committed as there is no notary change solution that works absent asking the reference state owner to change the notary. + +This can be mitigated by requesting that reference state owners distribute reference states for all notaries. This solution doesn't work for `OwnableState`s used as reference data as `OwnableState`s should be unique. However, in most cases it is anticipated that the users of `OwnableState`s as reference data will be the owners of those states. + +This solution introduces a new issue where nodes may store the same piece of reference data under different linear IDs. `TransactionBuilder`s would also need to know the required notary before a reference state is added. + +**Syndication of reference states** + +In the absence of data distribution groups, reference data must be manually transmitted to those that require it. Pulling might have the effect of DoS attacking nodes that own reference data used by many frequent users. Pushing requires reference data owners to be aware of all current users of the reference data. A temporary solution is required before data distribution groups are implemented. + +Initial thoughts are that pushing reference states is the better approach. + +**Interaction with encumbrances** + +It is likely not possible to reference encumbered states unless the encumbrance state is also referenced. For example, a cash state referenced for collateral reporting purposes may have been "seized" and thus encumbered by a regulator, thus cannot be counted for the collateral report. + +**What happens if a state is added to a transaction as an input as well as an input reference state?** + +An edge case where a developer might erroneously add the same StateRef as an input state _and_ input reference state. The effect is referring to reference data that immediately becomes out of date! This is an edge case that should be prevented as it is likely to confuse CorDapp developers. + +**Handling of update races.** + +Usage of a referenced state may race with an update to it. This would cause notarisation failure, however, the flow cannot simply loop and re-calculate the transaction because it has not necessarily seen the updated tx yet (it may be a slow broadcast). + +Therefore, it would make sense to extend the flows API with a new flow - call it WithReferencedStatesFlow that is given a set of LinearIDs and a factory that instantiates a subflow given a set of resolved StateAndRefs. + +It does the following: + +1. Checks that those linear IDs are in the vault and throws if not. +2. Resolves the linear IDs to the tip StateAndRefs. +3. Creates the subflow, passing in the resolved StateAndRefs to the factory, and then invokes it. +4. If the subflow throws a NotaryException because it tried to finalise and failed, that exception is caught and examined. If the failure was due to a conflict on a referenced state, the flow suspends until that state has been updated in the vault (there is an API to do wait for transaction already, but here the flow must wait for a state update). +5. Then it re-does the initial calculation, re-creates the subflow with the new resolved tips using the factory, and re-runs it as a new subflow. + +Care must be taken to handle progress tracking correctly in case of loops. + +## Complementary solutions + +See discussion of alternative approaches above in the "design decisions" section. + +## Final recommendation + +Proceed to Implementation + +TECHNICAL DESIGN +--- + +* Summary of changes to be included. +* Summary of outstanding issues to be included. \ No newline at end of file diff --git a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java index aaca60ab22..b72ed66979 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/FlowCookbookJava.java @@ -267,6 +267,10 @@ public class FlowCookbookJava { ------------------------------------------*/ progressTracker.setCurrentStep(OTHER_TX_COMPONENTS); + // Reference input states are constructed from StateAndRefs. + // DOCSTART 55 + ReferencedStateAndRef referenceState = ourStateAndRef.referenced(); + // DOCEND 55 // Output states are constructed from scratch. // DOCSTART 22 DummyState ourOutputState = new DummyState(); diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt index a35b8803d8..776b37a8d5 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FlowCookbook.kt @@ -267,6 +267,10 @@ class InitiatorFlow(val arg1: Boolean, val arg2: Int, private val counterparty: -----------------------------------------**/ progressTracker.currentStep = OTHER_TX_COMPONENTS + // Reference input states are constructed from StateAndRefs. + // DOCSTART 55 + val referenceState: ReferencedStateAndRef = ourStateAndRef.referenced() + // DOCEND 55 // Output states are constructed from scratch. // DOCSTART 22 val ourOutputState: DummyState = DummyState() diff --git a/docs/source/network-bootstrapper.rst b/docs/source/network-bootstrapper.rst index 5d28f87d1d..8d9dce1ad4 100644 --- a/docs/source/network-bootstrapper.rst +++ b/docs/source/network-bootstrapper.rst @@ -24,8 +24,7 @@ You can find out more about network maps and network parameters from :doc:`netwo Bootstrapping a test network ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The bootstrapper can be downloaded from https://downloads.corda.net/network-bootstrapper-VERSION.jar, where ``VERSION`` -is the Corda version. +The Corda Network Bootstrapper can be downloaded from `here `_. Create a directory containing a node config file, ending in "_node.conf", for each node you want to create. Then run the following command: diff --git a/node/src/main/kotlin/net/corda/node/services/api/CheckpointStorage.kt b/node/src/main/kotlin/net/corda/node/services/api/CheckpointStorage.kt index f5e5b62fc4..54d080121d 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/CheckpointStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/CheckpointStorage.kt @@ -21,10 +21,16 @@ import java.util.stream.Stream */ interface CheckpointStorage { /** - * Add a new checkpoint to the store. + * Add a checkpoint for a new id to the store. Will throw if there is already a checkpoint for this id */ fun addCheckpoint(id: StateMachineRunId, checkpoint: SerializedBytes) + /** + * Update an existing checkpoint. Will throw if there is not checkpoint for this id. + */ + fun updateCheckpoint(id: StateMachineRunId, checkpoint: SerializedBytes) + + /** * Remove existing checkpoint from the store. * @return whether the id matched a checkpoint that was removed. diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt index c8048c382c..8c6a1efc3e 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/DBCheckpointStorage.kt @@ -49,13 +49,22 @@ class DBCheckpointStorage : CheckpointStorage { ) override fun addCheckpoint(id: StateMachineRunId, checkpoint: SerializedBytes) { - currentDBSession().saveOrUpdate(DBCheckpoint().apply { + currentDBSession().save(DBCheckpoint().apply { checkpointId = id.uuid.toString() this.checkpoint = checkpoint.bytes log.debug { "Checkpoint $checkpointId, size=${this.checkpoint.size}" } }) } + override fun updateCheckpoint(id: StateMachineRunId, checkpoint: SerializedBytes) { + currentDBSession().update(DBCheckpoint().apply { + checkpointId = id.uuid.toString() + this.checkpoint = checkpoint.bytes + log.debug { "Checkpoint $checkpointId, size=${this.checkpoint.size}" } + }) + } + + override fun removeCheckpoint(id: StateMachineRunId): Boolean { val session = currentDBSession() val criteriaBuilder = session.criteriaBuilder diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt index a91a1d17c3..fe7a738f95 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt @@ -49,7 +49,7 @@ sealed class Action { /** * Persist the specified [checkpoint]. */ - data class PersistCheckpoint(val id: StateMachineRunId, val checkpoint: Checkpoint) : Action() + data class PersistCheckpoint(val id: StateMachineRunId, val checkpoint: Checkpoint, val isCheckpointUpdate: Boolean) : Action() /** * Remove the checkpoint corresponding to [id]. diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt index 5ae8d3171c..88384d44fb 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt @@ -107,7 +107,11 @@ class ActionExecutorImpl( @Suspendable private fun executePersistCheckpoint(action: Action.PersistCheckpoint) { val checkpointBytes = serializeCheckpoint(action.checkpoint) - checkpointStorage.addCheckpoint(action.id, checkpointBytes) + if (action.isCheckpointUpdate) { + checkpointStorage.updateCheckpoint(action.id, checkpointBytes) + } else { + checkpointStorage.addCheckpoint(action.id, checkpointBytes) + } checkpointingMeter.mark() checkpointSizesThisSecond.update(checkpointBytes.size.toLong()) var lastUpdateTime = lastBandwidthUpdate.get() diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TopLevelTransition.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TopLevelTransition.kt index 04f9776625..28bf2a21f4 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TopLevelTransition.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/TopLevelTransition.kt @@ -172,7 +172,7 @@ class TopLevelTransition( ) } else { actions.addAll(arrayOf( - Action.PersistCheckpoint(context.id, newCheckpoint), + Action.PersistCheckpoint(context.id, newCheckpoint, isCheckpointUpdate = currentState.isAnyCheckpointPersisted), Action.PersistDeduplicationFacts(currentState.pendingDeduplicationHandlers), Action.CommitTransaction, Action.AcknowledgeMessages(currentState.pendingDeduplicationHandlers), diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/UnstartedFlowTransition.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/UnstartedFlowTransition.kt index aeed37c370..5e3281effa 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/UnstartedFlowTransition.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/UnstartedFlowTransition.kt @@ -88,7 +88,7 @@ class UnstartedFlowTransition( private fun TransitionBuilder.createInitialCheckpoint() { actions.addAll(arrayOf( Action.CreateTransaction, - Action.PersistCheckpoint(context.id, currentState.checkpoint), + Action.PersistCheckpoint(context.id, currentState.checkpoint, isCheckpointUpdate = currentState.isAnyCheckpointPersisted), Action.PersistDeduplicationFacts(currentState.pendingDeduplicationHandlers), Action.CommitTransaction, Action.AcknowledgeMessages(currentState.pendingDeduplicationHandlers) diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt index 86a099b4c1..1c71d50e13 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt @@ -49,8 +49,9 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAut verify() checkAllComponentsVisible(ComponentGroupEnum.INPUTS_GROUP) checkAllComponentsVisible(ComponentGroupEnum.TIMEWINDOW_GROUP) + checkAllComponentsVisible(ComponentGroupEnum.REFERENCES_GROUP) } - TransactionParts(tx.id, tx.inputs, tx.timeWindow, tx.notary) + TransactionParts(tx.id, tx.inputs, tx.timeWindow, tx.notary, tx.references) } is ContractUpgradeFilteredTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary) is NotaryChangeWireTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary) diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt index 5f7cb332b9..6f35575636 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/PersistentUniquenessProvider.kt @@ -113,10 +113,12 @@ class PersistentUniquenessProvider(val clock: Clock) : UniquenessProvider, Singl txId: SecureHash, callerIdentity: Party, requestSignature: NotarisationRequestSignature, - timeWindow: TimeWindow?) { + timeWindow: TimeWindow?, + references: List + ) { mutex.locked { logRequest(txId, callerIdentity, requestSignature) - val conflictingStates = findAlreadyCommitted(states, commitLog) + val conflictingStates = findAlreadyCommitted(states, references, commitLog) if (conflictingStates.isNotEmpty()) { handleConflicts(txId, conflictingStates) } else { @@ -136,12 +138,23 @@ class PersistentUniquenessProvider(val clock: Clock) : UniquenessProvider, Singl session.persist(request) } - private fun findAlreadyCommitted(states: List, commitLog: AppendOnlyPersistentMap): LinkedHashMap { + private fun findAlreadyCommitted( + states: List, + references: List, + commitLog: AppendOnlyPersistentMap + ): LinkedHashMap { val conflictingStates = LinkedHashMap() - for (inputState in states) { - val consumingTx = commitLog[inputState] - if (consumingTx != null) conflictingStates[inputState] = StateConsumptionDetails(consumingTx.sha256()) + + fun checkConflicts(toCheck: List, type: StateConsumptionDetails.ConsumedStateType) { + return toCheck.forEach { stateRef -> + val consumingTx = commitLog[stateRef] + if (consumingTx != null) conflictingStates[stateRef] = StateConsumptionDetails(consumingTx.sha256(), type) + } } + + checkConflicts(states, StateConsumptionDetails.ConsumedStateType.INPUT_STATE) + checkConflicts(references, StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE) + return conflictingStates } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLog.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLog.kt index a6e0e87879..86abdc8cde 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLog.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftTransactionCommitLog.kt @@ -55,12 +55,13 @@ class RaftTransactionCommitLog( createMap: () -> AppendOnlyPersistentMap, E, EK> ) : StateMachine(), Snapshottable { object Commands { - class CommitTransaction( + class CommitTransaction @JvmOverloads constructor( val states: List, val txId: SecureHash, val requestingParty: String, val requestSignature: ByteArray, - val timeWindow: TimeWindow? = null + val timeWindow: TimeWindow? = null, + val references: List = emptyList() ) : Command { override fun compaction(): Command.CompactionMode { // The FULL compaction mode retains the command in the log until it has been stored and applied on all @@ -83,18 +84,21 @@ class RaftTransactionCommitLog( /** Commits the input states for the transaction as specified in the given [Commands.CommitTransaction]. */ fun commitTransaction(raftCommit: Commit): NotaryError? { + val conflictingStates = LinkedHashMap() + + fun checkConflict(states: List, type: StateConsumptionDetails.ConsumedStateType) = states.forEach { stateRef -> + map[stateRef]?.let { conflictingStates[stateRef] = StateConsumptionDetails(it.second.sha256(), type) } + } + raftCommit.use { val index = it.index() return db.transaction { val commitCommand = raftCommit.command() logRequest(commitCommand) - val states = commitCommand.states val txId = commitCommand.txId - log.debug("State machine commit: storing entries with keys (${states.joinToString()})") - val conflictingStates = LinkedHashMap() - for (state in states) { - map[state]?.let { conflictingStates[state] = StateConsumptionDetails(it.second.sha256()) } - } + log.debug("State machine commit: attempting to store entries with keys (${commitCommand.states.joinToString()})") + checkConflict(commitCommand.states, StateConsumptionDetails.ConsumedStateType.INPUT_STATE) + checkConflict(commitCommand.references, StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE) if (conflictingStates.isNotEmpty()) { if (isConsumedByTheSameTx(commitCommand.txId.sha256(), conflictingStates)) { null @@ -105,9 +109,9 @@ class RaftTransactionCommitLog( } else { val outsideTimeWindowError = validateTimeWindow(clock.instant(), commitCommand.timeWindow) if (outsideTimeWindowError == null) { - val entries = states.map { it to Pair(index, txId) }.toMap() + val entries = commitCommand.states.map { it to Pair(index, txId) }.toMap() map.putAll(entries) - log.debug { "Successfully committed all input states: $states" } + log.debug { "Successfully committed all input states: ${commitCommand.states}" } null } else { outsideTimeWindowError diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt index e90268145b..f9b6737489 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftUniquenessProvider.kt @@ -200,14 +200,17 @@ class RaftUniquenessProvider( txId: SecureHash, callerIdentity: Party, requestSignature: NotarisationRequestSignature, - timeWindow: TimeWindow?) { + timeWindow: TimeWindow?, + references: List + ) { log.debug { "Attempting to commit input states: ${states.joinToString()}" } val commitCommand = CommitTransaction( states, txId, callerIdentity.name.toString(), requestSignature.serialize().bytes, - timeWindow + timeWindow, + references ) val commitError = client.submit(commitCommand).get() if (commitError != null) throw NotaryInternalException(commitError) diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt index 1611734add..8064afe729 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt @@ -48,7 +48,7 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor resolveAndContractVerify(stx) verifySignatures(stx) val timeWindow: TimeWindow? = if (stx.coreTransaction is WireTransaction) stx.tx.timeWindow else null - return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!) + return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!, stx.references) } catch (e: Exception) { throw when (e) { is TransactionVerificationException, diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index c5b6e23049..20fcea0e1a 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -501,12 +501,12 @@ class NodeVaultService( @Throws(VaultQueryException::class) override fun _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class): DataFeed, Vault.Update> { - return database.transaction { + return mutex.locked { concurrentBox.exclusive { val snapshotResults = _queryBy(criteria, paging, sorting, contractStateType) val updates: Observable> = uncheckedCast(_updatesPublisher.bufferUntilSubscribed() - .filter { it.containsType(contractStateType, snapshotResults.stateTypes) } - .map { filterContractStates(it, contractStateType) }) + .filter { it.containsType(contractStateType, snapshotResults.stateTypes) } + .map { filterContractStates(it, contractStateType) }) DataFeed(snapshotResults, updates) } } @@ -564,4 +564,4 @@ class NodeVaultService( } return myInterfaces } -} \ No newline at end of file +} diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt index 606b5ebf2d..5b535e051a 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/MyCustomNotaryService.kt @@ -62,7 +62,7 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating verifySignatures(stx) resolveAndContractVerify(stx) val timeWindow: TimeWindow? = if (stx.coreTransaction is WireTransaction) stx.tx.timeWindow else null - return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!) + return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!, stx.references) } catch (e: Exception) { throw when (e) { is TransactionVerificationException, diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPExceptions.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPExceptions.kt index dbc0cad4f0..ea81fb0d69 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPExceptions.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPExceptions.kt @@ -1,7 +1,82 @@ package net.corda.serialization.internal.amqp +import net.corda.core.internal.VisibleForTesting +import org.slf4j.Logger import java.io.NotSerializableException import java.lang.reflect.Type -class SyntheticParameterException(val type: Type) : NotSerializableException("Type '${type.typeName} has synthetic " +/** + * Not a public property so will have to use reflection + */ +private fun Throwable.setMessage(newMsg: String) { + val detailMessageField = Throwable::class.java.getDeclaredField("detailMessage") + detailMessageField.isAccessible = true + detailMessageField.set(this, newMsg) +} + +/** + * Utility function which helps tracking the path in the object graph when exceptions are thrown. + * Since there might be a chain of nested calls it is useful to record which part of the graph caused an issue. + * Path information is added to the message of the exception being thrown. + */ +internal inline fun ifThrowsAppend(strToAppendFn: () -> String, block: () -> T): T { + try { + return block() + } catch (th: Throwable) { + if (th is AMQPNotSerializableException) { + th.classHierarchy.add(strToAppendFn()) + } else { + th.setMessage("${strToAppendFn()} -> ${th.message}") + } + throw th + } +} + +class AMQPNoTypeNotSerializableException( + msg: String, + mitigation: String = msg +) : AMQPNotSerializableException(Class::class.java, msg, mitigation, mutableListOf("Unknown")) + +/** + * The purpose of the [AMQPNotSerializableException] is to encapsulate internal serialization errors + * within the serialization framework and to capture errors when building serializer objects + * that will aid in debugging whilst also allowing "user helpful" information to be communicated + * outward to end users. + * + * @property type the class that failed to serialize + * @property msg the specific error + * @property mitigation information useful to an end user. + * @property classHierarchy represents the call hierarchy to the point of error. This is useful + * when debugging a deeply nested class that fails to serialize as that classes position in + * the class hierarchy is preserved, otherwise that information is lost. This list is automatically + * updated by the [ifThrowsAppend] function used within this library. + */ +open class AMQPNotSerializableException( + val type: Type, + val msg: String, + val mitigation: String = msg, + val classHierarchy : MutableList = mutableListOf(type.typeName) +) : NotSerializableException(msg) { + @Suppress("Unused") + constructor(type: Type) : this (type, "type=${type.typeName} is not serializable") + + @VisibleForTesting + fun errorMessage(direction: String) : String { + return "Serialization failed direction=\"$direction\", type=\"${type.typeName}\", " + + "msg=\"$msg\", " + + "ClassChain=\"${classHierarchy.asReversed().joinToString(" -> ")}\"" + } + + fun log(direction: String, logger: Logger) { + logger.error(errorMessage(direction)) + + // if debug is enabled print the stack, the exception we allow to escape + // will be printed into the log anyway by the handling thread + logger.debug("", cause) + } +} + +class SyntheticParameterException(type: Type) : AMQPNotSerializableException( + type, + "Type '${type.typeName} has synthetic " + "fields and is likely a nested inner class. This is not support by the Corda AMQP serialization framework") \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt index 3b10dcd638..a46353664d 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ArraySerializer.kt @@ -18,7 +18,6 @@ import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data -import java.io.NotSerializableException import java.lang.reflect.Type /** @@ -100,11 +99,11 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory) ): Any { if (obj is List<*>) { return obj.map { input.readObjectOrNull(it, schemas, elementType, context) }.toArrayOfType(elementType) - } else throw NotSerializableException("Expected a List but found $obj") + } else throw AMQPNotSerializableException(type, "Expected a List but found $obj") } open fun List.toArrayOfType(type: Type): Any { - val elementType = type.asClass() ?: throw NotSerializableException("Unexpected array element type $type") + val elementType = type.asClass() ?: throw AMQPNotSerializableException(type, "Unexpected array element type $type") val list = this return java.lang.reflect.Array.newInstance(elementType, this.size).apply { (0..lastIndex).forEach { java.lang.reflect.Array.set(this, it, list[it]) } @@ -116,7 +115,7 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory) // the array since Kotlin won't allow an implicit cast from Int (as they're stored as 16bit ints) to Char class CharArraySerializer(factory: SerializerFactory) : ArraySerializer(Array::class.java, factory) { override fun List.toArrayOfType(type: Type): Any { - val elementType = type.asClass() ?: throw NotSerializableException("Unexpected array element type $type") + val elementType = type.asClass() ?: throw AMQPNotSerializableException(type, "Unexpected array element type $type") val list = this return java.lang.reflect.Array.newInstance(elementType, this.size).apply { (0..lastIndex).forEach { java.lang.reflect.Array.set(this, it, (list[it] as Int).toChar()) } @@ -170,7 +169,11 @@ class PrimCharArraySerializer(factory: SerializerFactory) : PrimArraySerializer( } override fun List.toArrayOfType(type: Type): Any { - val elementType = type.asClass() ?: throw NotSerializableException("Unexpected array element type $type") + val elementType = type.asClass() ?: throw AMQPNotSerializableException( + type, + "Unexpected array element type $type", + "blob is corrupt") + val list = this return java.lang.reflect.Array.newInstance(elementType, this.size).apply { val array = this diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt index 5dafefb790..cc57c3b2bd 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CollectionSerializer.kt @@ -16,7 +16,6 @@ import net.corda.core.serialization.SerializationContext import net.corda.core.utilities.NonEmptySet import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data -import java.io.NotSerializableException import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import java.util.* @@ -45,7 +44,10 @@ class CollectionSerializer(private val declaredType: ParameterizedType, factory: )) private fun findConcreteType(clazz: Class<*>): (List<*>) -> Collection<*> { - return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported collection type $clazz.") + return supportedTypes[clazz] ?: throw AMQPNotSerializableException( + clazz, + "Unsupported collection type $clazz.", + "Supported Collections are ${supportedTypes.keys.joinToString(",")}") } fun deriveParameterizedType(declaredType: Type, declaredClass: Class<*>, actualClass: Class<*>?): ParameterizedType { @@ -58,7 +60,10 @@ class CollectionSerializer(private val declaredType: ParameterizedType, factory: return deriveParametrizedType(declaredType, collectionClass) } - throw NotSerializableException("Cannot derive collection type for declaredType: '$declaredType', declaredClass: '$declaredClass', actualClass: '$actualClass'") + throw AMQPNotSerializableException( + declaredType, + "Cannot derive collection type for declaredType: '$declaredType', " + + "declaredClass: '$declaredClass', actualClass: '$actualClass'") } private fun deriveParametrizedType(declaredType: Type, collectionClass: Class>): ParameterizedType = diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt index 09e2c47da9..4271ee62f5 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/CorDappCustomSerializer.kt @@ -17,7 +17,6 @@ import net.corda.core.serialization.SerializationCustomSerializer import net.corda.serialization.internal.amqp.SerializerFactory.Companion.nameForType import org.apache.qpid.proton.amqp.Symbol import org.apache.qpid.proton.codec.Data -import java.io.NotSerializableException import java.lang.reflect.Type import kotlin.reflect.jvm.javaType import kotlin.reflect.jvm.jvmErasure @@ -66,7 +65,9 @@ class CorDappCustomSerializer( init { if (types.size != 2) { - throw NotSerializableException("Unable to determine serializer parent types") + throw AMQPNotSerializableException( + CorDappCustomSerializer::class.java, + "Unable to determine serializer parent types") } } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt index 5ced730276..2c1cfbadae 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializationInput.kt @@ -39,25 +39,32 @@ data class ObjectAndEnvelope(val obj: T, val envelope: Envelope) * instances and threads. */ @KeepForDJVM -class DeserializationInput @JvmOverloads constructor(private val serializerFactory: SerializerFactory, - private val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist) { +class DeserializationInput @JvmOverloads constructor( + private val serializerFactory: SerializerFactory, + private val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist +) { private val objectHistory: MutableList = mutableListOf() private val logger = loggerFor() companion object { @VisibleForTesting - @Throws(NotSerializableException::class) - fun withDataBytes(byteSequence: ByteSequence, encodingWhitelist: EncodingWhitelist, task: (ByteBuffer) -> T): T { + @Throws(AMQPNoTypeNotSerializableException::class) + fun withDataBytes( + byteSequence: ByteSequence, + encodingWhitelist: EncodingWhitelist, + task: (ByteBuffer) -> T + ) : T { // Check that the lead bytes match expected header val amqpSequence = amqpMagic.consume(byteSequence) - ?: throw NotSerializableException("Serialization header does not match.") + ?: throw AMQPNoTypeNotSerializableException("Serialization header does not match.") var stream: InputStream = ByteBufferInputStream(amqpSequence) try { while (true) { when (SectionId.reader.readFrom(stream)) { SectionId.ENCODING -> { val encoding = CordaSerializationEncoding.reader.readFrom(stream) - encodingWhitelist.acceptEncoding(encoding) || throw NotSerializableException(encodingNotPermittedFormat.format(encoding)) + encodingWhitelist.acceptEncoding(encoding) || + throw AMQPNoTypeNotSerializableException(encodingNotPermittedFormat.format(encoding)) stream = encoding.wrap(stream) } SectionId.DATA_AND_STOP, SectionId.ALT_DATA_AND_STOP -> return task(stream.asByteBuffer()) @@ -68,29 +75,40 @@ class DeserializationInput @JvmOverloads constructor(private val serializerFacto } } - @Throws(NotSerializableException::class) + @Throws(AMQPNoTypeNotSerializableException::class) fun getEnvelope(byteSequence: ByteSequence, encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist): Envelope { return withDataBytes(byteSequence, encodingWhitelist) { dataBytes -> val data = Data.Factory.create() val expectedSize = dataBytes.remaining() - if (data.decode(dataBytes) != expectedSize.toLong()) throw NotSerializableException("Unexpected size of data") + if (data.decode(dataBytes) != expectedSize.toLong()) { + throw AMQPNoTypeNotSerializableException( + "Unexpected size of data", + "Blob is corrupted!.") + } Envelope.get(data) } } } - @Throws(NotSerializableException::class) + @Throws(AMQPNoTypeNotSerializableException::class) fun getEnvelope(byteSequence: ByteSequence) = getEnvelope(byteSequence, encodingWhitelist) - @Throws(NotSerializableException::class) + @Throws( + AMQPNotSerializableException::class, + AMQPNoTypeNotSerializableException::class) inline fun deserialize(bytes: SerializedBytes, context: SerializationContext): T = deserialize(bytes, T::class.java, context) - @Throws(NotSerializableException::class) + @Throws( + AMQPNotSerializableException::class, + AMQPNoTypeNotSerializableException::class) private fun des(generator: () -> R): R { try { return generator() + } catch (amqp : AMQPNotSerializableException) { + amqp.log("Deserialize", logger) + throw NotSerializableException(amqp.mitigation) } catch (nse: NotSerializableException) { throw nse } catch (t: Throwable) { @@ -143,12 +161,15 @@ class DeserializationInput @JvmOverloads constructor(private val serializerFacto // It must be a reference to an instance that has already been read, cheaply and quickly returning it by reference. val objectIndex = (obj.described as UnsignedInteger).toInt() if (objectIndex !in 0..objectHistory.size) - throw NotSerializableException("Retrieval of existing reference failed. Requested index $objectIndex " + + throw AMQPNotSerializableException( + type, + "Retrieval of existing reference failed. Requested index $objectIndex " + "is outside of the bounds for the list of size: ${objectHistory.size}") val objectRetrieved = objectHistory[objectIndex] if (!objectRetrieved::class.java.isSubClassOf(type.asClass()!!)) { - throw NotSerializableException( + throw AMQPNotSerializableException( + type, "Existing reference type mismatch. Expected: '$type', found: '${objectRetrieved::class.java}' " + "@ $objectIndex") } @@ -160,8 +181,11 @@ class DeserializationInput @JvmOverloads constructor(private val serializerFacto val serializer = serializerFactory.get(obj.descriptor, schemas) if (SerializerFactory.AnyType != type && serializer.type != type && with(serializer.type) { !isSubClassOf(type) && !materiallyEquivalentTo(type) - }) { - throw NotSerializableException("Described type with descriptor ${obj.descriptor} was " + + } + ) { + throw AMQPNotSerializableException( + type, + "Described type with descriptor ${obj.descriptor} was " + "expected to be of type $type but was ${serializer.type}") } serializer.readObject(obj.described, schemas, this, context) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializedParameterizedType.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializedParameterizedType.kt index 643d6ab453..9114c985d5 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializedParameterizedType.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/DeserializedParameterizedType.kt @@ -23,13 +23,19 @@ import java.util.* * of the JDK implementation which we use as the textual format in the AMQP schema. */ @KeepForDJVM -class DeserializedParameterizedType(private val rawType: Class<*>, private val params: Array, private val ownerType: Type? = null) : ParameterizedType { +class DeserializedParameterizedType( + private val rawType: Class<*>, + private val params: Array, + private val ownerType: Type? = null +) : ParameterizedType { init { if (params.isEmpty()) { - throw NotSerializableException("Must be at least one parameter type in a ParameterizedType") + throw AMQPNotSerializableException(rawType, "Must be at least one parameter type in a ParameterizedType") } if (params.size != rawType.typeParameters.size) { - throw NotSerializableException("Expected ${rawType.typeParameters.size} for ${rawType.name} but found ${params.size}") + throw AMQPNotSerializableException( + rawType, + "Expected ${rawType.typeParameters.size} for ${rawType.name} but found ${params.size}") } } @@ -52,10 +58,11 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p val paramTypes = ArrayList() val pos = parseTypeList("$name>", paramTypes, cl) if (pos <= name.length) { - throw NotSerializableException("Malformed string form of ParameterizedType. Unexpected '>' at character position $pos of $name.") + throw AMQPNoTypeNotSerializableException( + "Malformed string form of ParameterizedType. Unexpected '>' at character position $pos of $name.") } if (paramTypes.size != 1) { - throw NotSerializableException("Expected only one type, but got $paramTypes") + throw AMQPNoTypeNotSerializableException("Expected only one type, but got $paramTypes") } return paramTypes[0] } @@ -80,7 +87,7 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p if (!typeName.isEmpty()) { types += makeType(typeName, cl) } else if (needAType) { - throw NotSerializableException("Expected a type, not ','") + throw AMQPNoTypeNotSerializableException("Expected a type, not ','") } typeStart = pos needAType = true @@ -90,7 +97,7 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p if (!typeName.isEmpty()) { types += makeType(typeName, cl) } else if (needAType) { - throw NotSerializableException("Expected a type, not '>'") + throw AMQPNoTypeNotSerializableException("Expected a type, not '>'") } return pos } else { @@ -100,11 +107,11 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p if (params[pos].isWhitespace()) { typeStart = ++pos } else if (!needAType) { - throw NotSerializableException("Not expecting a type") + throw AMQPNoTypeNotSerializableException("Not expecting a type") } else if (params[pos] == '?') { pos++ } else if (!params[pos].isJavaIdentifierStart()) { - throw NotSerializableException("Invalid character at start of type: ${params[pos]}") + throw AMQPNoTypeNotSerializableException("Invalid character at start of type: ${params[pos]}") } else { pos++ } @@ -115,12 +122,13 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p } else if (!skippingWhitespace && (params[pos] == '.' || params[pos].isJavaIdentifierPart())) { pos++ } else { - throw NotSerializableException("Invalid character ${params[pos]} in middle of type $params at idx $pos") + throw AMQPNoTypeNotSerializableException( + "Invalid character ${params[pos]} in middle of type $params at idx $pos") } } } } - throw NotSerializableException("Missing close generics '>'") + throw AMQPNoTypeNotSerializableException("Missing close generics '>'") } private fun makeType(typeName: String, cl: ClassLoader): Type { @@ -134,9 +142,15 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p return DeserializedParameterizedType(makeType(rawTypeName, cl) as Class<*>, args.toTypedArray(), null) } - private fun parseTypeParams(params: String, startPos: Int, paramTypes: MutableList, cl: ClassLoader, depth: Int): Int { + private fun parseTypeParams( + params: String, + startPos: Int, + paramTypes: MutableList, + cl: ClassLoader, + depth: Int + ): Int { if (depth == MAX_DEPTH) { - throw NotSerializableException("Maximum depth of nested generics reached: $depth") + throw AMQPNoTypeNotSerializableException("Maximum depth of nested generics reached: $depth") } return startPos + parseTypeList(params.substring(startPos), paramTypes, cl, depth) } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt index 8286bf9846..ef3838340a 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumEvolutionSerializer.kt @@ -122,7 +122,9 @@ class EnumEvolutionSerializer( .associateBy({ it.value.toInt() }, { conversions[it.name] })) if (ordinals.filterNot { serialisedOrds[it.value] == it.key }.isNotEmpty()) { - throw NotSerializableException("Constants have been reordered, additions must be appended to the end") + throw AMQPNotSerializableException( + new.type, + "Constants have been reordered, additions must be appended to the end") } return EnumEvolutionSerializer(new.type, factory, conversions, ordinals) @@ -135,7 +137,7 @@ class EnumEvolutionSerializer( val enumName = (obj as List<*>)[0] as String if (enumName !in conversions) { - throw NotSerializableException("No rule to evolve enum constant $type::$enumName") + throw AMQPNotSerializableException(type, "No rule to evolve enum constant $type::$enumName") } return type.asClass()!!.enumConstants[ordinals[conversions[enumName]]!!] diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt index 7dde00a548..bd5aae679f 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EnumSerializer.kt @@ -47,8 +47,9 @@ class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: Seria val fromOrd = type.asClass()!!.enumConstants[enumOrd] as Enum<*>? if (enumName != fromOrd?.name) { - throw NotSerializableException("Deserializing obj as enum $type with value $enumName.$enumOrd but " - + "ordinality has changed") + throw AMQPNotSerializableException( + type, + "Deserializing obj as enum $type with value $enumName.$enumOrd but ordinality has changed") } return fromOrd } @@ -56,7 +57,7 @@ class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: Seria override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput, context: SerializationContext, debugIndent: Int ) { - if (obj !is Enum<*>) throw NotSerializableException("Serializing $obj as enum when it isn't") + if (obj !is Enum<*>) throw AMQPNotSerializableException(type, "Serializing $obj as enum when it isn't") data.withDescribed(typeNotation.descriptor) { withList { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Envelope.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Envelope.kt index fe7541ca5a..f89052de2a 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Envelope.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Envelope.kt @@ -41,7 +41,8 @@ data class Envelope(val obj: Any?, val schema: Schema, val transformsSchema: Tra fun get(data: Data): Envelope { val describedType = data.`object` as DescribedType if (describedType.descriptor != DESCRIPTOR) { - throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}, should be $DESCRIPTOR.") + throw AMQPNoTypeNotSerializableException( + "Unexpected descriptor ${describedType.descriptor}, should be $DESCRIPTOR.") } val list = describedType.described as List<*> @@ -50,7 +51,8 @@ data class Envelope(val obj: Any?, val schema: Schema, val transformsSchema: Tra val transformSchema: Any? = when (list.size) { ENVELOPE_WITHOUT_TRANSFORMS -> null ENVELOPE_WITH_TRANSFORMS -> list[TRANSFORMS_SCHEMA_IDX] - else -> throw NotSerializableException("Malformed list, bad length of ${list.size} (should be 2 or 3)") + else -> throw AMQPNoTypeNotSerializableException( + "Malformed list, bad length of ${list.size} (should be 2 or 3)") } return newInstance(listOf(list[BLOB_IDX], Schema.get(list[SCHEMA_IDX]!!), @@ -67,7 +69,8 @@ data class Envelope(val obj: Any?, val schema: Schema, val transformsSchema: Tra val transformSchema = when (list.size) { ENVELOPE_WITHOUT_TRANSFORMS -> TransformsSchema.newInstance(null) ENVELOPE_WITH_TRANSFORMS -> list[TRANSFORMS_SCHEMA_IDX] as TransformsSchema - else -> throw NotSerializableException("Malformed list, bad length of ${list.size} (should be 2 or 3)") + else -> throw AMQPNoTypeNotSerializableException( + "Malformed list, bad length of ${list.size} (should be 2 or 3)") } return Envelope(list[BLOB_IDX], list[SCHEMA_IDX] as Schema, transformSchema) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt index 00d8062e91..d9c86c72ff 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt @@ -42,8 +42,8 @@ abstract class EvolutionSerializer( clazz: Type, factory: SerializerFactory, protected val oldReaders: Map, - override val kotlinConstructor: KFunction?) : ObjectSerializer(clazz, factory) { - + override val kotlinConstructor: KFunction? +) : ObjectSerializer(clazz, factory) { // explicitly set as empty to indicate it's unused by this type of serializer override val propertySerializers = PropertySerializersEvolution() @@ -147,7 +147,8 @@ abstract class EvolutionSerializer( if ((isKotlin && !it.value.type.isMarkedNullable) || (!isKotlin && isJavaPrimitive(it.value.type.jvmErasure.java)) ) { - throw NotSerializableException( + throw AMQPNotSerializableException( + new.type, "New parameter \"${it.value.name}\" is mandatory, should be nullable for evolution " + "to work, isKotlinClass=$isKotlin type=${it.value.type}") } @@ -190,7 +191,7 @@ abstract class EvolutionSerializer( OldParam(-1, PropertySerializer.make(it.name, EvolutionPropertyReader(), it.getTypeAsClass(factory.classloader), factory)) } catch (e: ClassNotFoundException) { - throw NotSerializableException(e.message) + throw AMQPNotSerializableException(new.type, e.message ?: "") } } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt index ebac797dda..142f3c7134 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/FingerPrinter.kt @@ -110,11 +110,12 @@ class SerializerFingerPrinter : FingerPrinter { return if ((type in alreadySeen) && (type !== SerializerFactory.AnyType) && (type !is TypeVariable<*>) - && (type !is WildcardType)) { + && (type !is WildcardType) + ) { hasher.putUnencodedChars(ALREADY_SEEN_HASH) } else { alreadySeen += type - try { + ifThrowsAppend({ type.typeName }) { when (type) { is ParameterizedType -> { // Hash the rawType + params @@ -168,8 +169,8 @@ class SerializerFingerPrinter : FingerPrinter { } else { hasher.fingerprintWithCustomSerializerOrElse(factory!!, type, type) { if (type.kotlinObjectInstance != null) { - // TODO: name collision is too likely for kotlin objects, we need to introduce some reference - // to the CorDapp but maybe reference to the JAR in the short term. + // TODO: name collision is too likely for kotlin objects, we need to introduce some + // reference to the CorDapp but maybe reference to the JAR in the short term. hasher.putUnencodedChars(type.name) } else { fingerprintForObject(type, type, alreadySeen, hasher, factory!!, debugIndent + 1) @@ -182,12 +183,8 @@ class SerializerFingerPrinter : FingerPrinter { fingerprintForType(type.genericComponentType, contextType, alreadySeen, hasher, debugIndent + 1).putUnencodedChars(ARRAY_HASH) } - else -> throw NotSerializableException("Don't know how to hash") + else -> throw AMQPNotSerializableException(type, "Don't know how to hash") } - } catch (e: NotSerializableException) { - val msg = "${e.message} -> $type" - logger.error(msg, e) - throw NotSerializableException(msg) } } } @@ -201,7 +198,7 @@ class SerializerFingerPrinter : FingerPrinter { debugIndent: Int = 0): Hasher { // Hash the class + properties + interfaces val name = type.asClass()?.name - ?: throw NotSerializableException("Expected only Class or ParameterizedType but found $type") + ?: throw AMQPNotSerializableException(type, "Expected only Class or ParameterizedType but found $type") propertiesForSerialization(constructorForDeserialization(type), contextType ?: type, factory) .serializationOrder diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt index 30b78d925e..6e1d9df844 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/MapSerializer.kt @@ -50,7 +50,7 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial )) private fun findConcreteType(clazz: Class<*>): MapCreationFunction { - return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported map type $clazz.") + return supportedTypes[clazz] ?: throw AMQPNotSerializableException(clazz, "Unsupported map type $clazz.") } fun deriveParameterizedType(declaredType: Type, declaredClass: Class<*>, actualClass: Class<*>?): ParameterizedType { @@ -64,7 +64,8 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial return deriveParametrizedType(declaredType, mapClass) } - throw NotSerializableException("Cannot derive map type for declaredType: '$declaredType', declaredClass: '$declaredClass', actualClass: '$actualClass'") + throw AMQPNotSerializableException(declaredType, + "Cannot derive map type for declaredType=\"$declaredType\", declaredClass=\"$declaredClass\", actualClass=\"$actualClass\"") } private fun deriveParametrizedType(declaredType: Type, collectionClass: Class>): ParameterizedType = @@ -98,7 +99,8 @@ class MapSerializer(private val declaredType: ParameterizedType, factory: Serial type: Type, output: SerializationOutput, context: SerializationContext, - debugIndent: Int) = ifThrowsAppend({ declaredType.typeName }) { + debugIndent: Int + ) = ifThrowsAppend({ declaredType.typeName }) { obj.javaClass.checkSupportedMapType() // Write described data.withDescribed(typeNotation.descriptor) { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt index 002ea7488d..b52c5cca8a 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/ObjectSerializer.kt @@ -73,7 +73,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS if (propertySerializers.size != javaConstructor?.parameterCount && javaConstructor?.parameterCount ?: 0 > 0 ) { - throw NotSerializableException("Serialization constructor for class $type expects " + throw AMQPNotSerializableException(type, "Serialization constructor for class $type expects " + "${javaConstructor?.parameterCount} parameters but we have ${propertySerializers.size} " + "properties to serialize.") } @@ -96,7 +96,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS context: SerializationContext): Any = ifThrowsAppend({ clazz.typeName }) { if (obj is List<*>) { if (obj.size > propertySerializers.size) { - throw NotSerializableException("Too many properties in described type $typeName") + throw AMQPNotSerializableException(type, "Too many properties in described type $typeName") } return if (propertySerializers.byConstructor) { @@ -105,7 +105,7 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS readObjectBuildViaSetters(obj, schemas, input, context) } } else { - throw NotSerializableException("Body of described type is unexpected $obj") + throw AMQPNotSerializableException(type, "Body of described type is unexpected $obj") } } @@ -130,7 +130,8 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS context: SerializationContext): Any = ifThrowsAppend({ clazz.typeName }) { logger.trace { "Calling setter based construction for ${clazz.typeName}" } - val instance: Any = javaConstructor?.newInstanceUnwrapped() ?: throw NotSerializableException( + val instance: Any = javaConstructor?.newInstanceUnwrapped() ?: throw AMQPNotSerializableException( + type, "Failed to instantiate instance of object $clazz") // read the properties out of the serialised form, since we're invoking the setters the order we @@ -159,13 +160,15 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS logger.trace { "Calling constructor: '$javaConstructor' with properties '$properties'" } if (properties.size != javaConstructor?.parameterCount) { - throw NotSerializableException("Serialization constructor for class $type expects " + throw AMQPNotSerializableException(type, "Serialization constructor for class $type expects " + "${javaConstructor?.parameterCount} parameters but we have ${properties.size} " + "serialized properties.") } return javaConstructor?.newInstanceUnwrapped(*properties.toTypedArray()) - ?: throw NotSerializableException("Attempt to deserialize an interface: $clazz. Serialized form is invalid.") + ?: throw AMQPNotSerializableException( + type, + "Attempt to deserialize an interface: $clazz. Serialized form is invalid.") } private fun Constructor.newInstanceUnwrapped(vararg args: Any?): T { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializers.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializers.kt index 7ae011af08..8f26582e35 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializers.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/PropertySerializers.kt @@ -201,7 +201,7 @@ abstract class PropertySerializers( is PropertyAccessorGetterSetter -> PropertySerializersSetter(serializationOrder) null -> PropertySerializersNoProperties() else -> { - throw NotSerializableException("Unknown Property Accessor type, cannot create set") + throw AMQPNoTypeNotSerializableException("Unknown Property Accessor type, cannot create set") } } } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt index 1b37c71653..de39d81d57 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/Schema.kt @@ -230,7 +230,7 @@ data class RestrictedType(override val name: String, fun get(describedType: DescribedType): RestrictedType { if (describedType.descriptor != DESCRIPTOR) { - throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.") + throw AMQPNoTypeNotSerializableException("Unexpected descriptor ${describedType.descriptor}.") } val list = describedType.described as List<*> return newInstance(listOf(list[0], list[1], list[2], list[3], Descriptor.get(list[4]!!), (list[5] as List<*>).map { Choice.get(it!!) })) @@ -307,7 +307,7 @@ data class ReferencedObject(private val refCounter: Int) : DescribedType { fun get(obj: Any): ReferencedObject { val describedType = obj as DescribedType if (describedType.descriptor != DESCRIPTOR) { - throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.") + throw AMQPNoTypeNotSerializableException("Unexpected descriptor ${describedType.descriptor}.") } return newInstance(describedType.described) } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt index d5728542ba..df7663ca19 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationHelper.kt @@ -51,18 +51,24 @@ fun constructorForDeserialization(type: Type): KFunction? { for (kotlinConstructor in kotlinConstructors) { if (preferredCandidate == null && kotlinConstructors.size == 1) { preferredCandidate = kotlinConstructor - } else if (preferredCandidate == null && kotlinConstructors.size == 2 && hasDefault && kotlinConstructor.parameters.isNotEmpty()) { + } else if (preferredCandidate == null && + kotlinConstructors.size == 2 && + hasDefault && + kotlinConstructor.parameters.isNotEmpty() + ) { preferredCandidate = kotlinConstructor } else if (kotlinConstructor.findAnnotation() != null) { if (annotatedCount++ > 0) { - throw NotSerializableException("More than one constructor for $clazz is annotated with @ConstructorForDeserialization.") + throw AMQPNotSerializableException( + type, + "More than one constructor for $clazz is annotated with @ConstructorForDeserialization.") } preferredCandidate = kotlinConstructor } } return preferredCandidate?.apply { isAccessible = true } - ?: throw NotSerializableException("No constructor for deserialization found for $clazz.") + ?: throw AMQPNotSerializableException(type, "No constructor for deserialization found for $clazz.") } else { return null } @@ -259,13 +265,14 @@ internal fun propertiesForSerializationFromConstructor( // with the case we don't know the case of A when the parameter doesn't match a property // but has a getter val matchingProperty = classProperties[name] ?: classProperties[name.capitalize()] - ?: throw NotSerializableException( + ?: throw AMQPNotSerializableException(type, "Constructor parameter - \"$name\" - doesn't refer to a property of \"$clazz\"") // If the property has a getter we'll use that to retrieve it's value from the instance, if it doesn't // *for *know* we switch to a reflection based method val propertyReader = if (matchingProperty.getter != null) { - val getter = matchingProperty.getter ?: throw NotSerializableException( + val getter = matchingProperty.getter ?: throw AMQPNotSerializableException( + type, "Property has no getter method for - \"$name\" - of \"$clazz\". If using Java and the parameter name" + "looks anonymous, check that you have the -parameters option specified in the " + "Java compiler. Alternately, provide a proxy serializer " @@ -273,7 +280,8 @@ internal fun propertiesForSerializationFromConstructor( val returnType = resolveTypeVariables(getter.genericReturnType, type) if (!constructorParamTakesReturnTypeOfGetter(returnType, getter.genericReturnType, param.value)) { - throw NotSerializableException( + throw AMQPNotSerializableException( + type, "Property - \"$name\" - has type \"$returnType\" on \"$clazz\" but differs from constructor " + "parameter type \"${param.value.type.javaType}\"") } @@ -281,7 +289,8 @@ internal fun propertiesForSerializationFromConstructor( Pair(PublicPropertyReader(getter), returnType) } else { val field = classProperties[name]!!.field - ?: throw NotSerializableException("No property matching constructor parameter named - \"$name\" - " + + ?: throw AMQPNotSerializableException(type, + "No property matching constructor parameter named - \"$name\" - " + "of \"$clazz\". If using Java, check that you have the -parameters option specified " + "in the Java compiler. Alternately, provide a proxy serializer " + "(SerializationCustomSerializer) if recompiling isn't an option") @@ -314,22 +323,28 @@ fun propertiesForSerializationFromSetters( if (getter == null || setter == null) return@forEach if (setter.parameterCount != 1) { - throw NotSerializableException("Defined setter for parameter ${property.value.field?.name} " + - "takes too many arguments") + throw AMQPNotSerializableException( + type, + "Defined setter for parameter ${property.value.field?.name} takes too many arguments") } val setterType = setter.genericParameterTypes[0]!! if ((property.value.field != null) && - (!(TypeToken.of(property.value.field?.genericType!!).isSupertypeOf(setterType)))) { - throw NotSerializableException("Defined setter for parameter ${property.value.field?.name} " + + (!(TypeToken.of(property.value.field?.genericType!!).isSupertypeOf(setterType))) + ) { + throw AMQPNotSerializableException( + type, + "Defined setter for parameter ${property.value.field?.name} " + "takes parameter of type $setterType yet underlying type is " + "${property.value.field?.genericType!!}") } // Make sure the getter returns the same type (within inheritance bounds) the setter accepts. if (!(TypeToken.of(getter.genericReturnType).isSupertypeOf(setterType))) { - throw NotSerializableException("Defined setter for parameter ${property.value.field?.name} " + + throw AMQPNotSerializableException( + type, + "Defined setter for parameter ${property.value.field?.name} " + "takes parameter of type $setterType yet the defined getter returns a value of type " + "${getter.returnType} [${getter.genericReturnType}]") } @@ -446,7 +461,9 @@ fun resolveTypeVariables(actualType: Type, contextType: Type?): Type { SerializerFactory.AnyType } else if (bounds.size == 1) { resolveTypeVariables(bounds[0], contextType) - } else throw NotSerializableException("Got bounded type $actualType but only support single bound.") + } else throw AMQPNotSerializableException( + actualType, + "Got bounded type $actualType but only support single bound.") } else { resolvedType } @@ -488,7 +505,7 @@ internal fun Type.asParameterizedType(): ParameterizedType { return when (this) { is Class<*> -> this.asParameterizedType() is ParameterizedType -> this - else -> throw NotSerializableException("Don't know how to convert to ParameterizedType") + else -> throw AMQPNotSerializableException(this, "Don't know how to convert to ParameterizedType") } } @@ -509,32 +526,13 @@ internal enum class CommonPropertyNames { IncludeInternalInfo, } -/** - * Utility function which helps tracking the path in the object graph when exceptions are thrown. - * Since there might be a chain of nested calls it is useful to record which part of the graph caused an issue. - * Path information is added to the message of the exception being thrown. - */ -internal inline fun ifThrowsAppend(strToAppendFn: () -> String, block: () -> T): T { - try { - return block() - } catch (th: Throwable) { - th.setMessage("${strToAppendFn()} -> ${th.message}") - throw th - } -} -/** - * Not a public property so will have to use reflection - */ -private fun Throwable.setMessage(newMsg: String) { - val detailMessageField = Throwable::class.java.getDeclaredField("detailMessage") - detailMessageField.isAccessible = true - detailMessageField.set(this, newMsg) -} fun ClassWhitelist.requireWhitelisted(type: Type) { if (!this.isWhitelisted(type.asClass()!!)) { - throw NotSerializableException("Class \"$type\" is not on the whitelist or annotated with @CordaSerializable.") + throw AMQPNotSerializableException( + type, + "Class \"$type\" is not on the whitelist or annotated with @CordaSerializable.") } } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationOutput.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationOutput.kt index 767fadcce2..65a15562c8 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationOutput.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializationOutput.kt @@ -14,6 +14,7 @@ import net.corda.core.KeepForDJVM import net.corda.core.serialization.SerializationContext import net.corda.core.serialization.SerializationEncoding import net.corda.core.serialization.SerializedBytes +import net.corda.core.utilities.contextLogger import net.corda.serialization.internal.CordaSerializationEncoding import net.corda.serialization.internal.SectionId import net.corda.serialization.internal.byteArrayOutput @@ -41,6 +42,10 @@ open class SerializationOutput @JvmOverloads constructor( internal val serializerFactory: SerializerFactory, private val encoding: SerializationEncoding? = null ) { + companion object { + private val logger = contextLogger() + } + private val objectHistory: MutableMap = IdentityHashMap() private val serializerHistory: MutableSet> = LinkedHashSet() internal val schemaHistory: MutableSet = LinkedHashSet() @@ -54,11 +59,16 @@ open class SerializationOutput @JvmOverloads constructor( fun serialize(obj: T, context: SerializationContext): SerializedBytes { try { return _serialize(obj, context) + } catch (amqp: AMQPNotSerializableException) { + amqp.log("Serialize", logger) + throw NotSerializableException(amqp.mitigation) } finally { andFinally() } } + // NOTE: No need to handle AMQPNotSerializableExceptions here as this is an internal + // only / testing function and it doesn't matter if they escape @Throws(NotSerializableException::class) fun serializeAndReturnSchema(obj: T, context: SerializationContext): BytesAndSchemas { try { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt index c60a378601..a4e1aabd65 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt @@ -128,7 +128,8 @@ open class SerializerFactory( // can be useful to enable but will be *extremely* chatty if you do logger.trace { "Get Serializer for $actualClass ${declaredType.typeName}" } - val declaredClass = declaredType.asClass() ?: throw NotSerializableException( + val declaredClass = declaredType.asClass() ?: throw AMQPNotSerializableException( + declaredType, "Declared types of $declaredType are not supported.") val actualType: Type = inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType @@ -215,9 +216,12 @@ open class SerializerFactory( val endType = DeserializedParameterizedType(actualClass, actualClass.typeParameters) val resolvedType = resolver.resolveType(endType) resolvedType - } else throw NotSerializableException("No inheritance path between actual $actualClass and declared $declaredType.") + } else throw AMQPNotSerializableException(declaredType, + "No inheritance path between actual $actualClass and declared $declaredType.") } else actualClass - } else throw NotSerializableException("Found object of type $actualClass in a property expecting $declaredType") + } else throw AMQPNotSerializableException( + declaredType, + "Found object of type $actualClass in a property expecting $declaredType") } // Stop when reach declared type or return null if we don't find it. @@ -327,7 +331,7 @@ open class SerializerFactory( // prevent carpenter exceptions escaping into the world, convert things into a nice // NotSerializableException for when this escapes over the wire - throw NotSerializableException(e.name) + NotSerializableException(e.name) } processSchema(schemaAndDescriptor, true) } @@ -352,7 +356,9 @@ open class SerializerFactory( private fun processCompositeType(typeNotation: CompositeType): AMQPSerializer { // TODO: class loader logic, and compare the schema. val type = typeForName(typeNotation.name, classloader) - return get(type.asClass() ?: throw NotSerializableException("Unable to build composite type for $type"), type) + return get( + type.asClass() ?: throw AMQPNotSerializableException(type, "Unable to build composite type for $type"), + type) } private fun makeClassSerializer( @@ -364,13 +370,15 @@ open class SerializerFactory( if (clazz.isSynthetic) { // Explicitly ban synthetic classes, we have no way of recreating them when deserializing. This also // captures Lambda expressions and other anonymous functions - throw NotSerializableException(type.typeName) + throw AMQPNotSerializableException( + type, + "Serializer does not support synthetic classes") } else if (isPrimitive(clazz)) { AMQPPrimitiveSerializer(clazz) } else { findCustomSerializer(clazz, declaredType) ?: run { if (onlyCustomSerializers) { - throw NotSerializableException("Only allowing custom serializers") + throw AMQPNotSerializableException(type, "Only allowing custom serializers") } if (type.isArray()) { // Don't need to check the whitelist since each element will come back through the whitelisting process. @@ -395,7 +403,6 @@ open class SerializerFactory( } private fun doFindCustomSerializer(key: CustomSerializersCacheKey): AMQPSerializer? { - val (clazz, declaredType) = key // e.g. Imagine if we provided a Map serializer this way, then it won't work if the declared type is @@ -481,7 +488,7 @@ open class SerializerFactory( is GenericArrayType -> "${nameForType(type.genericComponentType)}[]" is WildcardType -> "?" is TypeVariable<*> -> "?" - else -> throw NotSerializableException("Unable to render type $type to a string.") + else -> throw AMQPNotSerializableException(type, "Unable to render type $type to a string.") } private fun typeForName(name: String, classloader: ClassLoader): Type { @@ -492,7 +499,7 @@ open class SerializerFactory( } else if (elementType is Class<*>) { java.lang.reflect.Array.newInstance(elementType, 0).javaClass } else { - throw NotSerializableException("Not able to deserialize array type: $name") + throw AMQPNoTypeNotSerializableException("Not able to deserialize array type: $name") } } else if (name.endsWith("[p]")) { // There is no need to handle the ByteArray case as that type is coercible automatically @@ -506,7 +513,7 @@ open class SerializerFactory( "double[p]" -> DoubleArray::class.java "short[p]" -> ShortArray::class.java "long[p]" -> LongArray::class.java - else -> throw NotSerializableException("Not able to deserialize array type: $name") + else -> throw AMQPNoTypeNotSerializableException("Not able to deserialize array type: $name") } } else { DeserializedParameterizedType.make(name, classloader) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt index f20fb5fe34..f07bf2341b 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/TransformsSchema.kt @@ -41,7 +41,7 @@ abstract class Transform : DescribedType { val describedType = obj as DescribedType if (describedType.descriptor != DESCRIPTOR) { - throw NotSerializableException("Unexpected descriptor ${describedType.descriptor}.") + throw AMQPNoTypeNotSerializableException("Unexpected descriptor ${describedType.descriptor}.") } return describedType.described @@ -231,7 +231,8 @@ data class TransformsSchema(val types: Map, ClassSerializer.ClassProxy>(Class::class.java, ClassProxy::class.java, factory) { - override fun toProxy(obj: Class<*>): ClassProxy = ClassProxy(obj.name) +class ClassSerializer( + factory: SerializerFactory +) : CustomSerializer.Proxy, ClassSerializer.ClassProxy>( + Class::class.java, + ClassProxy::class.java, + factory +) { + companion object { + private val logger = contextLogger() + } - override fun fromProxy(proxy: ClassProxy): Class<*> = Class.forName(proxy.className, true, factory.classloader) + override fun toProxy(obj: Class<*>): ClassProxy { + logger.trace { "serializer=custom, type=ClassSerializer, name=\"${obj.name}\", action=toProxy" } + return ClassProxy(obj.name) + } + + override fun fromProxy(proxy: ClassProxy): Class<*> { + logger.trace { "serializer=custom, type=ClassSerializer, name=\"${proxy.className}\", action=fromProxy" } + + return try { + Class.forName(proxy.className, true, factory.classloader) + } catch (e: ClassNotFoundException) { + throw AMQPNotSerializableException( + type, + "Could not instantiate ${proxy.className} - not on the classpath", + "${proxy.className} was not found by the node, check the Node containing the CorDapp that " + + "implements ${proxy.className} is loaded and on the Classpath", + mutableListOf(proxy.className)) + } + } @KeepForDJVM data class ClassProxy(val className: String) diff --git a/serialization/src/test/java/net/corda/serialization/internal/ForbiddenLambdaSerializationTests.java b/serialization/src/test/java/net/corda/serialization/internal/ForbiddenLambdaSerializationTests.java index 0161d07051..d6f2bcb0d0 100644 --- a/serialization/src/test/java/net/corda/serialization/internal/ForbiddenLambdaSerializationTests.java +++ b/serialization/src/test/java/net/corda/serialization/internal/ForbiddenLambdaSerializationTests.java @@ -14,6 +14,7 @@ import com.google.common.collect.Maps; import net.corda.core.serialization.SerializationContext; import net.corda.core.serialization.SerializationFactory; import net.corda.core.serialization.SerializedBytes; +import net.corda.serialization.internal.amqp.AMQPNotSerializableException; import net.corda.serialization.internal.amqp.SchemaKt; import net.corda.testing.core.SerializationEnvironmentRule; import org.junit.Before; @@ -53,7 +54,7 @@ public final class ForbiddenLambdaSerializationTests { assertThat(throwable) .isNotNull() .isInstanceOf(NotSerializableException.class) - .hasMessageContaining(getClass().getName()); + .hasMessageContaining("Serializer does not support synthetic classes"); }); } @@ -71,7 +72,7 @@ public final class ForbiddenLambdaSerializationTests { assertThat(throwable) .isNotNull() .isInstanceOf(NotSerializableException.class) - .hasMessageContaining(getClass().getName()); + .hasMessageContaining("Serializer does not support synthetic classes"); }); } diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/AMQPExceptionsTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/AMQPExceptionsTests.kt new file mode 100644 index 0000000000..b1caa03ecf --- /dev/null +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/AMQPExceptionsTests.kt @@ -0,0 +1,130 @@ +package net.corda.serialization.internal.amqp + +import org.assertj.core.api.Assertions +import org.junit.Test +import java.io.NotSerializableException +import kotlin.test.assertEquals + +class AMQPExceptionsTests { + + interface Runner { + fun run() + } + + data class A(val a: T, val throws: Boolean = false) : Runner { + override fun run() = ifThrowsAppend({ javaClass.name }) { + if (throws) { + throw AMQPNotSerializableException(A::class.java, "it went bang!") + } else { + a.run() + } + } + } + + data class B(val b: T, val throws: Boolean = false) : Runner { + override fun run() = ifThrowsAppend({ javaClass.name }) { + if (throws) { + throw NotSerializableException(javaClass.name) + } else { + b.run() + } + } + } + + data class C(val c: T, val throws: Boolean = false) : Runner { + override fun run() = ifThrowsAppend({ javaClass.name }) { + if (throws) { + throw NotSerializableException(javaClass.name) + } else { + c.run() + } + } + } + + data class END(val throws: Boolean = false) : Runner { + override fun run() = ifThrowsAppend({ "END" }) { + if (throws) { + throw NotSerializableException(javaClass.name) + } + } + } + + data class ENDAMQP(val throws: Boolean = false) : Runner { + override fun run() { + if (throws) { + throw AMQPNotSerializableException(javaClass, "End it all") + } + } + } + + private val aName get() = A::class.java.name + private val bName get() = B::class.java.name + private val cName get() = C::class.java.name + private val eName get() = END::class.java.name + private val eaName get() = ENDAMQP::class.java.name + + // if the exception is a normal not serializable exception we'll have manipulated the + // message + @Test + fun catchNotSerializable() { + fun catchAssert(msg: String, f: () -> Unit) { + Assertions.assertThatThrownBy { f() } + .isInstanceOf(NotSerializableException::class.java) + .hasMessageContaining(msg) + } + + catchAssert("$aName -> END") { + A(END(true)).run() + } + + catchAssert("$aName -> $aName -> END") { + A(A(END(true))).run() + } + + catchAssert("$aName -> $bName -> END") { + A(B(END(true))).run() + } + + catchAssert("$aName -> $bName -> $cName -> END") { + A(B(C(END(true)))).run() + } + } + + // However, if its a shiny new AMQPNotSerializable one, we have cool new toys, so + // lets make sure those are set + @Test + fun catchAMQPNotSerializable() { + fun catchAssert(stack: List, f: () -> Unit): AMQPNotSerializableException { + try { + f() + } catch (e: AMQPNotSerializableException) { + assertEquals(stack, e.classHierarchy) + return e + } + + throw Exception("FAILED") + } + + + catchAssert(listOf(ENDAMQP::class.java.name, aName)) { + A(ENDAMQP(true)).run() + }.apply { + assertEquals( + "Serialization failed direction=\"up\", type=\"$eaName\", msg=\"End it all\", " + + "ClassChain=\"$aName -> $eaName\"", + errorMessage("up")) + } + + catchAssert(listOf(ENDAMQP::class.java.name, aName, aName)) { + A(A(ENDAMQP(true))).run() + } + + catchAssert(listOf(ENDAMQP::class.java.name, bName, aName)) { + A(B(ENDAMQP(true))).run() + } + + catchAssert(listOf(ENDAMQP::class.java.name, cName, bName, aName)) { + A(B(C(ENDAMQP(true)))).run() + } + } +} diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumEvolvabilityTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumEvolvabilityTests.kt index 1f622570ce..ea0db3ea3a 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumEvolvabilityTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumEvolvabilityTests.kt @@ -16,7 +16,6 @@ import net.corda.serialization.internal.amqp.testutils.* import net.corda.testing.common.internal.ProjectStructure.projectRootDir import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy -import org.assertj.core.api.Condition import org.junit.Test import java.io.NotSerializableException import java.net.URI diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumTests.kt index 7dd7fac64f..483392e480 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/EnumTests.kt @@ -16,7 +16,6 @@ import net.corda.core.serialization.SerializedBytes import net.corda.serialization.internal.amqp.testutils.TestSerializationOutput import net.corda.serialization.internal.amqp.testutils.deserialize import net.corda.serialization.internal.amqp.testutils.deserializeAndReturnEnvelope -import net.corda.serialization.internal.amqp.testutils.serialize import net.corda.serialization.internal.amqp.testutils.serializeAndReturnSchema import net.corda.serialization.internal.amqp.testutils.testDefaultFactoryNoEvolution import net.corda.serialization.internal.amqp.testutils.testName diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/PrivatePropertyTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/PrivatePropertyTests.kt index d2a332f679..3cb72820a2 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/PrivatePropertyTests.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/PrivatePropertyTests.kt @@ -133,7 +133,7 @@ class PrivatePropertyTests { SerializationOutput(factory).serialize(c1) }.isInstanceOf(NotSerializableException::class.java).hasMessageContaining( "Defined setter for parameter a takes parameter of type class java.lang.String " + - "yet underlying type is int ") + "yet underlying type is int") } @Test diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt index 5b237b3002..d2e1994b43 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalMockNetwork.kt @@ -144,7 +144,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy, val notarySpecs: List = defaultParameters.notarySpecs, val testDirectory: Path = Paths.get("build", getTimestampAsDirectoryName()), - networkParameters: NetworkParameters = testNetworkParameters(), + val networkParameters: NetworkParameters = testNetworkParameters(), val defaultFactory: (MockNodeArgs, CordappLoader?) -> MockNode = { args, cordappLoader -> cordappLoader?.let { MockNode(args, it) } ?: MockNode(args) }, val cordappsForAllNodes: Set = emptySet()) { init { @@ -248,6 +248,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe // The network parameters must be serialised before starting any of the nodes networkParametersCopier = NetworkParametersCopier(networkParameters.copy(notaries = notaryInfos)) @Suppress("LeakingThis") + // Notary nodes need a platform version >= network min platform version. notaryNodes = createNotaries() } catch (t: Throwable) { stopNodes() @@ -264,10 +265,13 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe @VisibleForTesting internal open fun createNotaries(): List { + val version = VersionInfo(networkParameters.minimumPlatformVersion, "Mock release", "Mock revision", "Mock Vendor") return notarySpecs.map { (name, validating) -> - createNode(InternalMockNodeParameters(legalName = name, configOverrides = { - doReturn(NotaryConfig(validating)).whenever(it).notary - })) + createNode(InternalMockNodeParameters( + legalName = name, + configOverrides = { doReturn(NotaryConfig(validating)).whenever(it).notary }, + version = version + )) } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MutableTestCorDapp.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MutableTestCorDapp.kt index 28284e5de2..d06270c2cd 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MutableTestCorDapp.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/MutableTestCorDapp.kt @@ -15,7 +15,7 @@ internal class MutableTestCorDapp private constructor(override val name: String, private const val jarExtension = ".jar" private const val whitespace = " " private const val whitespaceReplacement = "_" - private val productionPathSegments = setOf<(String) -> String>({ "out${File.separator}production${File.separator}classes" }, { fullyQualifiedName -> "main${File.separator}${fullyQualifiedName.packageToPath()}" }) + private val productionPathSegments = setOf<(String) -> String>({ "out${File.separator}production${File.separator}classes" }, { fullyQualifiedName -> "main${File.separator}${fullyQualifiedName.packageToJarPath()}" }) private val excludedCordaPackages = setOf("net.corda.core", "net.corda.node") fun filterTestCorDappClass(fullyQualifiedName: String, url: URL): Boolean { diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt index 2ac02f8c92..6f755db697 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappsUtils.kt @@ -7,7 +7,6 @@ import net.corda.core.internal.outputStream import net.corda.node.internal.cordapp.createTestManifest import net.corda.testing.driver.TestCorDapp import org.apache.commons.io.IOUtils -import java.io.File import java.io.OutputStream import java.net.URI import java.net.URL @@ -136,7 +135,7 @@ fun simplifyScanPackages(scanPackages: Iterable): List { /** * Transforms a class or package name into a path segment. */ -internal fun String.packageToPath() = replace(".", File.separator) +internal fun String.packageToJarPath() = replace(".", "/") private fun Iterable.zip(outputStream: ZipOutputStream, willResourceBeAddedBeToCorDapp: (String, URL) -> Boolean): Boolean { @@ -177,7 +176,7 @@ internal sealed class JarEntryInfo(val fullyQualifiedName: String, val url: URL) */ class ClassJarEntryInfo(val clazz: Class<*>) : JarEntryInfo(clazz.name, clazz.classFileURL()) { - override val entryName = "${fullyQualifiedName.packageToPath()}$fileExtensionSeparator$classFileExtension" + override val entryName = "${fullyQualifiedName.packageToJarPath()}$fileExtensionSeparator$classFileExtension" } /** @@ -188,7 +187,7 @@ internal sealed class JarEntryInfo(val fullyQualifiedName: String, val url: URL) override val entryName: String get() { val extensionIndex = fullyQualifiedName.lastIndexOf(fileExtensionSeparator) - return "${fullyQualifiedName.substring(0 until extensionIndex).packageToPath()}${fullyQualifiedName.substring(extensionIndex)}" + return "${fullyQualifiedName.substring(0 until extensionIndex).packageToJarPath()}${fullyQualifiedName.substring(extensionIndex)}" } } @@ -206,7 +205,7 @@ internal sealed class JarEntryInfo(val fullyQualifiedName: String, val url: URL) private fun Class<*>.classFileURL(): URL { require(protectionDomain?.codeSource?.location != null) { "Invalid class $name for test CorDapp. Classes without protection domain cannot be referenced. This typically happens for Java / Kotlin types." } - return URI.create("${protectionDomain.codeSource.location}/${name.packageToPath()}$fileExtensionSeparator$classFileExtension".escaped()).toURL() + return URI.create("${protectionDomain.codeSource.location}/${name.packageToJarPath()}$fileExtensionSeparator$classFileExtension".escaped()).toURL() } private fun String.escaped(): String = this.replace(whitespace, whitespaceReplacement) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt index 2fe3c54399..44480b3a2b 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt @@ -13,8 +13,8 @@ package net.corda.testing.dsl import net.corda.core.DoNotImplement import net.corda.core.contracts.* import net.corda.core.cordapp.CordappProvider -import net.corda.core.crypto.* import net.corda.core.crypto.NullKeys.NULL_SIGNATURE +import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException import net.corda.core.identity.Party import net.corda.core.internal.UNKNOWN_UPLOADER @@ -24,9 +24,9 @@ import net.corda.core.node.ServicesForResolution import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction -import net.corda.testing.services.MockAttachmentStorage -import net.corda.testing.internal.MockCordappProvider import net.corda.testing.core.dummyCommand +import net.corda.testing.internal.MockCordappProvider +import net.corda.testing.services.MockAttachmentStorage import java.io.InputStream import java.security.PublicKey import java.util.* @@ -108,6 +108,12 @@ data class TestTransactionDSLInterpreter private constructor( transactionBuilder.addInputState(StateAndRef(state, stateRef)) } + override fun reference(stateRef: StateRef) { + val state = ledgerInterpreter.resolveStateRef(stateRef) + @Suppress("DEPRECATION") // Will remove when feature finalised. + transactionBuilder.addReferenceState(StateAndRef(state, stateRef).referenced()) + } + override fun output(contractClassName: ContractClassName, label: String?, notary: Party, @@ -303,7 +309,8 @@ data class TestLedgerDSLInterpreter private constructor( val wtx = value.transaction val ltx = wtx.toLedgerTransaction(services) ltx.verify() - val doubleSpend = wtx.inputs.intersect(usedInputs) + val allInputs = wtx.inputs union wtx.references + val doubleSpend = allInputs intersect usedInputs if (!doubleSpend.isEmpty()) { val txIds = mutableListOf(wtx.id) doubleSpend.mapTo(txIds) { it.txhash } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt index 5acc47c97a..15c137be4e 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt @@ -46,6 +46,12 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup { */ fun input(stateRef: StateRef) + /** + * Add a reference input state to the transaction. Note that [verifies] will resolve this reference. + * @param stateRef The input [StateRef]. + */ + fun reference(stateRef: StateRef) + /** * Adds an output to the transaction. * @param label An optional label that may be later used to retrieve the output probably in other transactions. @@ -98,6 +104,24 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup { * Underlying class for the transaction DSL. Do not instantiate directly, instead use the [transaction] function. * */ class TransactionDSL(interpreter: T, private val notary: Party) : TransactionDSLInterpreter by interpreter { + /** + * Looks up the output label and adds the found state as an reference input state. + * @param stateLabel The label of the output state specified when calling [TransactionDSLInterpreter.output] and friends. + */ + fun reference(stateLabel: String) = reference(retrieveOutputStateAndRef(ContractState::class.java, stateLabel).ref) + + /** + * Creates an [LedgerDSLInterpreter._unverifiedTransaction] with a single reference input state and adds its + * reference as in input to the current transaction. + * @param state The state to be added. + */ + fun reference(contractClassName: ContractClassName, state: ContractState) { + val transaction = ledgerInterpreter._unverifiedTransaction(null, TransactionBuilder(notary)) { + output(contractClassName, null, notary, null, AlwaysAcceptAttachmentConstraint, state) + } + reference(transaction.outRef(0).ref) + } + /** * Looks up the output label and adds the found state as an input. * @param stateLabel The label of the output state specified when calling [TransactionDSLInterpreter.output] and friends. @@ -110,7 +134,7 @@ class TransactionDSL(interpreter: T, private } /** - * Creates an [LedgerDSLInterpreter._unverifiedTransaction] with a single output state and adds it's reference as an + * Creates an [LedgerDSLInterpreter._unverifiedTransaction] with a single output state and adds its reference as an * input to the current transaction. * @param state The state to be added. */