mirror of
https://github.com/corda/corda.git
synced 2024-12-24 07:06:44 +00:00
CORDA-1010: Send a request signature in addition to a transaction to the notary (#2527)
CORDA-1010: Notary flow - clients now send a signature over a notarisation request in addition to the transaction. This will be logged by the notary to be able to prove that a particular party has requested the consumption of a particular state.
This commit is contained in:
parent
fee89c044f
commit
5b93abdc57
@ -1128,7 +1128,8 @@ public final class net.corda.core.flows.ContractUpgradeFlow extends java.lang.Ob
|
||||
public <init>(net.corda.core.contracts.StateAndRef, Class)
|
||||
@co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull protected net.corda.core.flows.AbstractStateReplacementFlow$UpgradeTx assembleTx()
|
||||
##
|
||||
public abstract class net.corda.core.flows.DataVendingFlow extends net.corda.core.flows.FlowLogic
|
||||
public class net.corda.core.flows.DataVendingFlow extends net.corda.core.flows.FlowLogic
|
||||
public <init>(net.corda.core.flows.FlowSession, Object)
|
||||
@co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.Nullable public Void call()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.flows.FlowSession getOtherSideSession()
|
||||
@org.jetbrains.annotations.NotNull public final Object getPayload()
|
||||
@ -1322,11 +1323,11 @@ public @interface net.corda.core.flows.InitiatingFlow
|
||||
@org.jetbrains.annotations.NotNull public String toString()
|
||||
##
|
||||
@net.corda.core.serialization.CordaSerializable public static final class net.corda.core.flows.NotaryError$General extends net.corda.core.flows.NotaryError
|
||||
public <init>(String)
|
||||
@org.jetbrains.annotations.NotNull public final String component1()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError$General copy(String)
|
||||
public <init>(Throwable)
|
||||
@org.jetbrains.annotations.NotNull public final Throwable component1()
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.flows.NotaryError$General copy(Throwable)
|
||||
public boolean equals(Object)
|
||||
@org.jetbrains.annotations.NotNull public final String getCause()
|
||||
@org.jetbrains.annotations.NotNull public final Throwable getCause()
|
||||
public int hashCode()
|
||||
@org.jetbrains.annotations.NotNull public String toString()
|
||||
##
|
||||
@ -1370,8 +1371,6 @@ public final class net.corda.core.flows.NotaryFlow extends java.lang.Object
|
||||
@co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull public List call()
|
||||
@org.jetbrains.annotations.NotNull public net.corda.core.utilities.ProgressTracker getProgressTracker()
|
||||
@co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull protected final net.corda.core.utilities.UntrustworthyData notarise(net.corda.core.identity.Party)
|
||||
@co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull protected net.corda.core.utilities.UntrustworthyData sendAndReceiveNonValidating(net.corda.core.identity.Party, net.corda.core.flows.FlowSession)
|
||||
@co.paralleluniverse.fibers.Suspendable @org.jetbrains.annotations.NotNull protected net.corda.core.utilities.UntrustworthyData sendAndReceiveValidating(net.corda.core.flows.FlowSession)
|
||||
@org.jetbrains.annotations.NotNull protected final List validateResponse(net.corda.core.utilities.UntrustworthyData, net.corda.core.identity.Party)
|
||||
public static final net.corda.core.flows.NotaryFlow$Client$Companion Companion
|
||||
##
|
||||
@ -2923,7 +2922,7 @@ public static final class net.corda.core.serialization.SingletonSerializationTok
|
||||
@org.jetbrains.annotations.NotNull public final net.corda.core.crypto.SecureHash getId()
|
||||
@org.jetbrains.annotations.NotNull public final String getReason()
|
||||
##
|
||||
@net.corda.core.DoNotImplement public abstract class net.corda.core.transactions.CoreTransaction extends net.corda.core.transactions.BaseTransaction
|
||||
@net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public abstract class net.corda.core.transactions.CoreTransaction extends net.corda.core.transactions.BaseTransaction
|
||||
public <init>()
|
||||
@org.jetbrains.annotations.NotNull public abstract List getInputs()
|
||||
##
|
||||
@ -3165,7 +3164,7 @@ public class net.corda.core.transactions.TransactionBuilder extends java.lang.Ob
|
||||
public abstract void verifyRequiredSignatures()
|
||||
public abstract void verifySignaturesExcept(Collection)
|
||||
##
|
||||
@net.corda.core.DoNotImplement public abstract class net.corda.core.transactions.TraversableTransaction extends net.corda.core.transactions.CoreTransaction
|
||||
@net.corda.core.serialization.CordaSerializable @net.corda.core.DoNotImplement public abstract class net.corda.core.transactions.TraversableTransaction extends net.corda.core.transactions.CoreTransaction
|
||||
public <init>(List)
|
||||
@org.jetbrains.annotations.NotNull public final List getAttachments()
|
||||
@org.jetbrains.annotations.NotNull public final List getAvailableComponentGroups()
|
||||
|
101
core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt
Normal file
101
core/src/main/kotlin/net/corda/core/flows/NotarisationRequest.kt
Normal file
@ -0,0 +1,101 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import java.security.InvalidKeyException
|
||||
import java.security.SignatureException
|
||||
|
||||
/**
|
||||
* A notarisation request specifies a list of states to consume and the id of the consuming transaction. Its primary
|
||||
* purpose is for notarisation traceability – a signature over the notarisation request, [NotarisationRequestSignature],
|
||||
* allows a notary to prove that a certain party requested the consumption of a particular state.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* 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].
|
||||
* 3) Locate the [NotarisationRequestSignature] for the transaction id. The signature will contain the signing public key.
|
||||
* 4) Demonstrate the signature verifies against the serialized request. The provided states are always sorted internally,
|
||||
* to ensure the serialization does not get affected by the order.
|
||||
*/
|
||||
@CordaSerializable
|
||||
class NotarisationRequest(statesToConsume: List<StateRef>, val transactionId: SecureHash) {
|
||||
companion object {
|
||||
/** Sorts in ascending order first by transaction hash, then by output index. */
|
||||
private val stateRefComparator = compareBy<StateRef>({ it.txhash }, { it.index })
|
||||
}
|
||||
|
||||
private val _statesToConsumeSorted = statesToConsume.sortedWith(stateRefComparator)
|
||||
|
||||
/** States this request specifies to be consumed. Sorted to ensure the serialized form does not get affected by the state order. */
|
||||
val statesToConsume: List<StateRef> get() = _statesToConsumeSorted // Getter required for AMQP serialization
|
||||
|
||||
/** Verifies the signature against this notarisation request. Checks that the signature is issued by the right party. */
|
||||
fun verifySignature(requestSignature: NotarisationRequestSignature, intendedSigner: Party) {
|
||||
val signature = requestSignature.digitalSignature
|
||||
if (intendedSigner.owningKey != signature.by) {
|
||||
val errorMessage = "Expected a signature by ${intendedSigner.owningKey}, but received by ${signature.by}}"
|
||||
throw NotaryException(NotaryError.RequestSignatureInvalid(IllegalArgumentException(errorMessage)))
|
||||
}
|
||||
// TODO: if requestSignature was generated over an old version of NotarisationRequest, we need to be able to
|
||||
// reserialize it in that version to get the exact same bytes. Modify the serialization logic once that's
|
||||
// available.
|
||||
val expectedSignedBytes = this.serialize().bytes
|
||||
verifyCorrectBytesSigned(signature, expectedSignedBytes)
|
||||
}
|
||||
|
||||
private fun verifyCorrectBytesSigned(signature: DigitalSignature.WithKey, bytes: ByteArray) {
|
||||
try {
|
||||
signature.verify(bytes)
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is InvalidKeyException, is SignatureException -> {
|
||||
val error = NotaryError.RequestSignatureInvalid(e)
|
||||
throw NotaryException(error)
|
||||
}
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around a digital signature used for notarisation requests.
|
||||
*
|
||||
* The [platformVersion] is required so the notary can verify the signature against the right version of serialized
|
||||
* bytes of the [NotarisationRequest]. Otherwise, the request may be rejected.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class NotarisationRequestSignature(val digitalSignature: DigitalSignature.WithKey, val platformVersion: Int)
|
||||
|
||||
/**
|
||||
* Container for the transaction and notarisation request signature.
|
||||
* This is the payload that gets sent by a client to a notary service for committing the input states of the [transaction].
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class NotarisationPayload(val transaction: Any, val requestSignature: NotarisationRequestSignature) {
|
||||
init {
|
||||
require(transaction is SignedTransaction || transaction is CoreTransaction) {
|
||||
"Unsupported transaction type in the notarisation payload: ${transaction.javaClass.simpleName}"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper for automatically casting the underlying [transaction] payload to a [SignedTransaction].
|
||||
* Should only be used by validating notaries.
|
||||
*/
|
||||
val signedTransaction get() = transaction as SignedTransaction
|
||||
/**
|
||||
* A helper for automatically casting the underlying [transaction] payload to a [CoreTransaction].
|
||||
* Should only be used by non-validating notaries.
|
||||
*/
|
||||
val coreTransaction get() = transaction as CoreTransaction
|
||||
}
|
@ -9,10 +9,12 @@ import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FetchDataFlow
|
||||
import net.corda.core.internal.generateSignature
|
||||
import net.corda.core.node.services.NotaryService
|
||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||
import net.corda.core.node.services.UniquenessProvider
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
@ -73,15 +75,17 @@ class NotaryFlow {
|
||||
return notaryParty
|
||||
}
|
||||
|
||||
/** Notarises the transaction with the [notaryParty], obtains the notary's signature(s). */
|
||||
@Throws(NotaryException::class)
|
||||
@Suspendable
|
||||
protected fun notarise(notaryParty: Party): UntrustworthyData<List<TransactionSignature>> {
|
||||
return try {
|
||||
val session = initiateFlow(notaryParty)
|
||||
val requestSignature = NotarisationRequest(stx.inputs, stx.id).generateSignature(serviceHub)
|
||||
if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
|
||||
sendAndReceiveValidating(session)
|
||||
sendAndReceiveValidating(session, requestSignature)
|
||||
} else {
|
||||
sendAndReceiveNonValidating(notaryParty, session)
|
||||
sendAndReceiveNonValidating(notaryParty, session, requestSignature)
|
||||
}
|
||||
} catch (e: NotaryException) {
|
||||
if (e.error is NotaryError.Conflict) {
|
||||
@ -92,21 +96,23 @@ class NotaryFlow {
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
protected open fun sendAndReceiveValidating(session: FlowSession): UntrustworthyData<List<TransactionSignature>> {
|
||||
subFlow(SendTransactionWithRetry(session, stx))
|
||||
private fun sendAndReceiveValidating(session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData<List<TransactionSignature>> {
|
||||
val payload = NotarisationPayload(stx, signature)
|
||||
subFlow(NotarySendTransactionFlow(session, payload))
|
||||
return session.receive()
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
protected open fun sendAndReceiveNonValidating(notaryParty: Party, session: FlowSession): UntrustworthyData<List<TransactionSignature>> {
|
||||
val tx: Any = if (stx.isNotaryChangeTransaction()) {
|
||||
private fun sendAndReceiveNonValidating(notaryParty: Party, session: FlowSession, signature: NotarisationRequestSignature): UntrustworthyData<List<TransactionSignature>> {
|
||||
val tx: CoreTransaction = if (stx.isNotaryChangeTransaction()) {
|
||||
stx.notaryChangeTx // Notary change transactions do not support filtering
|
||||
} else {
|
||||
stx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow || it == notaryParty })
|
||||
}
|
||||
return session.sendAndReceiveWithRetry(tx)
|
||||
return session.sendAndReceiveWithRetry(NotarisationPayload(tx, signature))
|
||||
}
|
||||
|
||||
/** Checks that the notary's signature(s) is/are valid. */
|
||||
protected fun validateResponse(response: UntrustworthyData<List<TransactionSignature>>, notaryParty: Party): List<TransactionSignature> {
|
||||
return response.unwrap { signatures ->
|
||||
signatures.forEach { validateSignature(it, stx.id, notaryParty) }
|
||||
@ -118,18 +124,18 @@ class NotaryFlow {
|
||||
check(sig.by in notaryParty.owningKey.keys) { "Invalid signer for the notary result" }
|
||||
sig.verify(txId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The [SendTransactionWithRetry] flow is equivalent to [SendTransactionFlow] but using [sendAndReceiveWithRetry]
|
||||
* instead of [sendAndReceive], [SendTransactionWithRetry] is intended to be use by the notary client only.
|
||||
* The [NotarySendTransactionFlow] flow is similar to [SendTransactionFlow], but uses [NotarisationPayload] as the
|
||||
* initial message, and retries message delivery.
|
||||
*/
|
||||
private class SendTransactionWithRetry(otherSideSession: FlowSession, stx: SignedTransaction) : SendTransactionFlow(otherSideSession, stx) {
|
||||
private class NotarySendTransactionFlow(otherSide: FlowSession, payload: NotarisationPayload) : DataVendingFlow(otherSide, payload) {
|
||||
@Suspendable
|
||||
override fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any): UntrustworthyData<FetchDataFlow.Request> {
|
||||
return otherSideSession.sendAndReceiveWithRetry(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A flow run by a notary service that handles notarisation requests.
|
||||
@ -186,10 +192,16 @@ class NotaryFlow {
|
||||
*/
|
||||
data class TransactionParts(val id: SecureHash, val inputs: List<StateRef>, val timestamp: TimeWindow?, val notary: Party?)
|
||||
|
||||
/**
|
||||
* Exception thrown by the notary service if any issues are encountered while trying to commit a transaction. The
|
||||
* underlying [error] specifies the cause of failure.
|
||||
*/
|
||||
class NotaryException(val error: NotaryError) : FlowException("Unable to notarise: $error")
|
||||
|
||||
/** Specifies the cause for notarisation request failure. */
|
||||
@CordaSerializable
|
||||
sealed class NotaryError {
|
||||
/** Occurs when one or more input states of transaction with [txId] have already been consumed by another transaction. */
|
||||
data class Conflict(val txId: SecureHash, val conflict: SignedData<UniquenessProvider.Conflict>) : NotaryError() {
|
||||
override fun toString() = "One or more input states for transaction $txId have been used in another transaction"
|
||||
}
|
||||
@ -199,18 +211,27 @@ sealed class NotaryError {
|
||||
override fun toString() = "Current time $currentTime is outside the time bounds specified by the transaction: $txTimeWindow"
|
||||
|
||||
companion object {
|
||||
@JvmField @Deprecated("Here only for binary compatibility purposes, do not use.")
|
||||
@JvmField
|
||||
@Deprecated("Here only for binary compatibility purposes, do not use.")
|
||||
val INSTANCE = TimeWindowInvalid(Instant.EPOCH, TimeWindow.fromOnly(Instant.EPOCH))
|
||||
}
|
||||
}
|
||||
|
||||
/** Occurs when the provided transaction fails to verify. */
|
||||
data class TransactionInvalid(val cause: Throwable) : NotaryError() {
|
||||
override fun toString() = cause.toString()
|
||||
}
|
||||
|
||||
/** Occurs when the transaction sent for notarisation is assigned to a different notary identity. */
|
||||
object WrongNotary : NotaryError()
|
||||
|
||||
data class General(val cause: String): NotaryError() {
|
||||
override fun toString() = cause
|
||||
/** Occurs when the notarisation request signature does not verify for the provided transaction. */
|
||||
data class RequestSignatureInvalid(val cause: Throwable) : NotaryError() {
|
||||
override fun toString() = "Request signature invalid: $cause"
|
||||
}
|
||||
|
||||
/** Occurs when the notary service encounters an unexpected issue or becomes temporarily unavailable. */
|
||||
data class General(val cause: Throwable) : NotaryError() {
|
||||
override fun toString() = cause.toString()
|
||||
}
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ open class SendTransactionFlow(otherSide: FlowSession, stx: SignedTransaction) :
|
||||
*/
|
||||
open class SendStateAndRefFlow(otherSideSession: FlowSession, stateAndRefs: List<StateAndRef<*>>) : DataVendingFlow(otherSideSession, stateAndRefs)
|
||||
|
||||
sealed class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any) : FlowLogic<Void?>() {
|
||||
open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any) : FlowLogic<Void?>() {
|
||||
@Suspendable
|
||||
protected open fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any) = otherSideSession.sendAndReceive<FetchDataFlow.Request>(payload)
|
||||
|
||||
|
@ -7,7 +7,11 @@ import net.corda.core.cordapp.CordappConfig
|
||||
import net.corda.core.cordapp.CordappContext
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.flows.NotarisationRequest
|
||||
import net.corda.core.flows.NotarisationRequestSignature
|
||||
import net.corda.core.flows.NotaryFlow
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
@ -382,3 +386,19 @@ fun ByteBuffer.copyBytes() = ByteArray(remaining()).also { get(it) }
|
||||
fun createCordappContext(cordapp: Cordapp, attachmentId: SecureHash?, classLoader: ClassLoader, config: CordappConfig): CordappContext {
|
||||
return CordappContext(cordapp, attachmentId, classLoader, config)
|
||||
}
|
||||
/** Verifies that the correct notarisation request was signed by the counterparty. */
|
||||
fun NotaryFlow.Service.validateRequest(request: NotarisationRequest, signature: NotarisationRequestSignature) {
|
||||
val requestingParty = otherSideSession.counterparty
|
||||
request.verifySignature(signature, requestingParty)
|
||||
// TODO: persist the signature for traceability. Do we need to persist the request as well?
|
||||
}
|
||||
|
||||
/** Creates a signature over the notarisation request using the legal identity key. */
|
||||
fun NotarisationRequest.generateSignature(serviceHub: ServiceHub): NotarisationRequestSignature {
|
||||
val serializedRequest = this.serialize().bytes
|
||||
val signature = with(serviceHub) {
|
||||
val myLegalIdentity = myInfo.legalIdentitiesAndCerts.first().owningKey
|
||||
keyManagementService.sign(serializedRequest, myLegalIdentity)
|
||||
}
|
||||
return NotarisationRequestSignature(signature, serviceHub.myInfo.platformVersion)
|
||||
}
|
@ -94,7 +94,7 @@ abstract class TrustedAuthorityNotaryService : NotaryService() {
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log.error("Internal error", e)
|
||||
throw NotaryException(NotaryError.General("Service unavailable, please try again later"))
|
||||
throw NotaryException(NotaryError.General(Exception("Service unavailable, please try again later")))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,12 +3,14 @@ package net.corda.core.transactions
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
|
||||
/**
|
||||
* A transaction with the minimal amount of information required to compute the unique transaction [id], and
|
||||
* resolve a [FullTransaction]. This type of transaction, wrapped in [SignedTransaction], gets transferred across the
|
||||
* wire and recorded to storage.
|
||||
*/
|
||||
@CordaSerializable
|
||||
abstract class CoreTransaction : BaseTransaction() {
|
||||
/** The inputs of this transaction, containing state references only **/
|
||||
abstract override val inputs: List<StateRef>
|
||||
|
@ -12,13 +12,19 @@ import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.NotaryException
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.NotarisationPayload
|
||||
import net.corda.core.flows.NotarisationRequest
|
||||
import net.corda.core.node.services.NotaryService
|
||||
import net.corda.core.node.services.UniquenessProvider
|
||||
import net.corda.core.schemas.PersistentStateRef
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.FilteredTransaction
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
import net.corda.node.services.config.BFTSMaRtConfiguration
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
@ -67,25 +73,25 @@ class BFTNonValidatingNotaryService(
|
||||
replicaHolder.getOrThrow() // It's enough to wait for the ServiceReplica constructor to return.
|
||||
}
|
||||
|
||||
fun commitTransaction(tx: Any, otherSide: Party) = client.commitTransaction(tx, otherSide)
|
||||
fun commitTransaction(payload: NotarisationPayload, otherSide: Party) = client.commitTransaction(payload, otherSide)
|
||||
|
||||
override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = ServiceFlow(otherPartySession, this)
|
||||
|
||||
private class ServiceFlow(val otherSideSession: FlowSession, val service: BFTNonValidatingNotaryService) : FlowLogic<Void?>() {
|
||||
@Suspendable
|
||||
override fun call(): Void? {
|
||||
val stx = otherSideSession.receive<FilteredTransaction>().unwrap { it }
|
||||
val signatures = commit(stx)
|
||||
val payload = otherSideSession.receive<NotarisationPayload>().unwrap { it }
|
||||
val signatures = commit(payload)
|
||||
otherSideSession.send(signatures)
|
||||
return null
|
||||
}
|
||||
|
||||
private fun commit(stx: FilteredTransaction): List<DigitalSignature> {
|
||||
val response = service.commitTransaction(stx, otherSideSession.counterparty)
|
||||
private fun commit(payload: NotarisationPayload): List<DigitalSignature> {
|
||||
val response = service.commitTransaction(payload, otherSideSession.counterparty)
|
||||
when (response) {
|
||||
is BFTSMaRt.ClusterResponse.Error -> throw NotaryException(response.error)
|
||||
is BFTSMaRt.ClusterResponse.Signatures -> {
|
||||
log.debug("All input states of transaction ${stx.id} have been committed")
|
||||
log.debug("All input states of transaction ${payload.coreTransaction.id} have been committed")
|
||||
return response.txSignatures
|
||||
}
|
||||
}
|
||||
@ -132,28 +138,34 @@ class BFTNonValidatingNotaryService(
|
||||
notaryIdentityKey: PublicKey) : BFTSMaRt.Replica(config, replicaId, createMap, services, notaryIdentityKey) {
|
||||
|
||||
override fun executeCommand(command: ByteArray): ByteArray {
|
||||
val request = command.deserialize<BFTSMaRt.CommitRequest>()
|
||||
val ftx = request.tx as FilteredTransaction
|
||||
val response = verifyAndCommitTx(ftx, request.callerIdentity)
|
||||
val commitRequest = command.deserialize<BFTSMaRt.CommitRequest>()
|
||||
verifyRequest(commitRequest)
|
||||
val response = verifyAndCommitTx(commitRequest.payload.coreTransaction, commitRequest.callerIdentity)
|
||||
return response.serialize().bytes
|
||||
}
|
||||
|
||||
fun verifyAndCommitTx(ftx: FilteredTransaction, callerIdentity: Party): BFTSMaRt.ReplicaResponse {
|
||||
private fun verifyAndCommitTx(transaction: CoreTransaction, callerIdentity: Party): BFTSMaRt.ReplicaResponse {
|
||||
return try {
|
||||
val id = ftx.id
|
||||
val inputs = ftx.inputs
|
||||
val notary = ftx.notary
|
||||
NotaryService.validateTimeWindow(services.clock, ftx.timeWindow)
|
||||
val id = transaction.id
|
||||
val inputs = transaction.inputs
|
||||
val notary = transaction.notary
|
||||
if (transaction is FilteredTransaction) NotaryService.validateTimeWindow(services.clock, transaction.timeWindow)
|
||||
if (notary !in services.myInfo.legalIdentities) throw NotaryException(NotaryError.WrongNotary)
|
||||
commitInputStates(inputs, id, callerIdentity)
|
||||
log.debug { "Inputs committed successfully, signing $id" }
|
||||
BFTSMaRt.ReplicaResponse.Signature(sign(ftx))
|
||||
BFTSMaRt.ReplicaResponse.Signature(sign(id))
|
||||
} catch (e: NotaryException) {
|
||||
log.debug { "Error processing transaction: ${e.error}" }
|
||||
BFTSMaRt.ReplicaResponse.Error(e.error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyRequest(commitRequest: BFTSMaRt.CommitRequest) {
|
||||
val transaction = commitRequest.payload.coreTransaction
|
||||
val notarisationRequest = NotarisationRequest(transaction.inputs, transaction.id)
|
||||
notarisationRequest.verifySignature(commitRequest.payload.requestSignature, commitRequest.callerIdentity)
|
||||
// TODO: persist the signature for traceability.
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
|
@ -17,6 +17,7 @@ import net.corda.core.crypto.*
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.NotaryException
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.NotarisationPayload
|
||||
import net.corda.core.internal.declaredField
|
||||
import net.corda.core.internal.toTypedArray
|
||||
import net.corda.core.node.services.UniquenessProvider
|
||||
@ -25,8 +26,6 @@ import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.FilteredTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
@ -52,7 +51,7 @@ import java.util.*
|
||||
object BFTSMaRt {
|
||||
/** Sent from [Client] to [Replica]. */
|
||||
@CordaSerializable
|
||||
data class CommitRequest(val tx: Any, val callerIdentity: Party)
|
||||
data class CommitRequest(val payload: NotarisationPayload, val callerIdentity: Party)
|
||||
|
||||
/** Sent from [Replica] to [Client]. */
|
||||
@CordaSerializable
|
||||
@ -101,13 +100,12 @@ object BFTSMaRt {
|
||||
* Sends a transaction commit request to the BFT cluster. The [proxy] will deliver the request to every
|
||||
* replica, and block until a sufficient number of replies are received.
|
||||
*/
|
||||
fun commitTransaction(transaction: Any, otherSide: Party): ClusterResponse {
|
||||
require(transaction is FilteredTransaction || transaction is SignedTransaction) { "Unsupported transaction type: ${transaction.javaClass.name}" }
|
||||
fun commitTransaction(payload: NotarisationPayload, otherSide: Party): ClusterResponse {
|
||||
awaitClientConnectionToCluster()
|
||||
cluster.waitUntilAllReplicasHaveInitialized()
|
||||
val requestBytes = CommitRequest(transaction, otherSide).serialize().bytes
|
||||
val requestBytes = CommitRequest(payload, otherSide).serialize().bytes
|
||||
val responseBytes = proxy.invokeOrdered(requestBytes)
|
||||
return responseBytes.deserialize<ClusterResponse>()
|
||||
return responseBytes.deserialize()
|
||||
}
|
||||
|
||||
/** A comparator to check if replies from two replicas are the same. */
|
||||
@ -242,12 +240,15 @@ object BFTSMaRt {
|
||||
}
|
||||
}
|
||||
|
||||
/** Generates a signature over an arbitrary array of bytes. */
|
||||
protected fun sign(bytes: ByteArray): DigitalSignature.WithKey {
|
||||
return services.database.transaction { services.keyManagementService.sign(bytes, notaryIdentityKey) }
|
||||
}
|
||||
|
||||
protected fun sign(filteredTransaction: FilteredTransaction): TransactionSignature {
|
||||
return services.database.transaction { services.createSignature(filteredTransaction, notaryIdentityKey) }
|
||||
/** Generates a transaction signature over the specified transaction [txId]. */
|
||||
protected fun sign(txId: SecureHash): TransactionSignature {
|
||||
val signableData = SignableData(txId, SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(notaryIdentityKey).schemeNumberID))
|
||||
return services.keyManagementService.sign(signableData, notaryIdentityKey)
|
||||
}
|
||||
|
||||
// TODO:
|
||||
|
@ -1,11 +1,15 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.contracts.ComponentGroupEnum
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.NotaryFlow
|
||||
import net.corda.core.flows.TransactionParts
|
||||
import net.corda.core.flows.NotarisationPayload
|
||||
import net.corda.core.flows.NotarisationRequest
|
||||
import net.corda.core.internal.validateRequest
|
||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.FilteredTransaction
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.utilities.unwrap
|
||||
@ -21,22 +25,30 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAut
|
||||
*/
|
||||
@Suspendable
|
||||
override fun receiveAndVerifyTx(): TransactionParts {
|
||||
val parts = otherSideSession.receive<Any>().unwrap {
|
||||
when (it) {
|
||||
is FilteredTransaction -> {
|
||||
it.verify()
|
||||
it.checkAllComponentsVisible(ComponentGroupEnum.INPUTS_GROUP)
|
||||
it.checkAllComponentsVisible(ComponentGroupEnum.TIMEWINDOW_GROUP)
|
||||
val notary = it.notary
|
||||
TransactionParts(it.id, it.inputs, it.timeWindow, notary)
|
||||
return otherSideSession.receive<NotarisationPayload>().unwrap { payload ->
|
||||
val transaction = payload.coreTransaction
|
||||
val request = NotarisationRequest(transaction.inputs, transaction.id)
|
||||
validateRequest(request, payload.requestSignature)
|
||||
extractParts(transaction)
|
||||
}
|
||||
is NotaryChangeWireTransaction -> TransactionParts(it.id, it.inputs, null, it.notary)
|
||||
}
|
||||
|
||||
private fun extractParts(tx: CoreTransaction): TransactionParts {
|
||||
return when (tx) {
|
||||
is FilteredTransaction -> {
|
||||
tx.apply {
|
||||
verify()
|
||||
checkAllComponentsVisible(ComponentGroupEnum.INPUTS_GROUP)
|
||||
checkAllComponentsVisible(ComponentGroupEnum.TIMEWINDOW_GROUP)
|
||||
}
|
||||
val notary = tx.notary
|
||||
TransactionParts(tx.id, tx.inputs, tx.timeWindow, notary)
|
||||
}
|
||||
is NotaryChangeWireTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary)
|
||||
else -> {
|
||||
throw IllegalArgumentException("Received unexpected transaction type: ${it::class.java.simpleName}," +
|
||||
throw IllegalArgumentException("Received unexpected transaction type: ${tx::class.java.simpleName}," +
|
||||
"expected either ${FilteredTransaction::class.java.simpleName} or ${NotaryChangeWireTransaction::class.java.simpleName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
return parts
|
||||
}
|
||||
}
|
@ -4,8 +4,14 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.flows.NotarisationPayload
|
||||
import net.corda.core.flows.NotarisationRequest
|
||||
import net.corda.core.internal.ResolveTransactionsFlow
|
||||
import net.corda.core.internal.validateRequest
|
||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionWithSignatures
|
||||
import net.corda.core.utilities.unwrap
|
||||
import java.security.SignatureException
|
||||
|
||||
/**
|
||||
@ -22,15 +28,15 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor
|
||||
@Suspendable
|
||||
override fun receiveAndVerifyTx(): TransactionParts {
|
||||
try {
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false))
|
||||
val stx = receiveTransaction()
|
||||
val notary = stx.notary
|
||||
checkNotary(notary)
|
||||
val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction())
|
||||
null
|
||||
else
|
||||
stx.tx.timeWindow
|
||||
val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub)
|
||||
checkSignatures(transactionWithSignatures)
|
||||
resolveAndContractVerify(stx)
|
||||
verifySignatures(stx)
|
||||
return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!)
|
||||
} catch (e: Exception) {
|
||||
throw when (e) {
|
||||
@ -41,6 +47,26 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun receiveTransaction(): SignedTransaction {
|
||||
return otherSideSession.receive<NotarisationPayload>().unwrap {
|
||||
val stx = it.signedTransaction
|
||||
validateRequest(NotarisationRequest(stx.inputs, stx.id), it.requestSignature)
|
||||
stx
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun resolveAndContractVerify(stx: SignedTransaction) {
|
||||
subFlow(ResolveTransactionsFlow(stx, otherSideSession))
|
||||
stx.verify(serviceHub, false)
|
||||
}
|
||||
|
||||
private fun verifySignatures(stx: SignedTransaction) {
|
||||
val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub)
|
||||
checkSignatures(transactionWithSignatures)
|
||||
}
|
||||
|
||||
private fun checkSignatures(tx: TransactionWithSignatures) {
|
||||
try {
|
||||
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
||||
|
@ -3,24 +3,35 @@ package net.corda.node.services.transactions
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.sign
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.NotaryException
|
||||
import net.corda.core.flows.NotaryFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.NotarisationPayload
|
||||
import net.corda.core.flows.NotarisationRequest
|
||||
import net.corda.core.flows.NotarisationRequestSignature
|
||||
import net.corda.core.internal.generateSignature
|
||||
import net.corda.core.messaging.MessageRecipients
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.node.services.api.StartedNodeServices
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.node.services.messaging.Message
|
||||
import net.corda.node.services.statemachine.InitialSessionMessage
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.dummyCommand
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockNodeParameters
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.startFlow
|
||||
import net.corda.testing.node.*
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
@ -34,6 +45,7 @@ import kotlin.test.assertTrue
|
||||
class NotaryServiceTests {
|
||||
private lateinit var mockNet: MockNetwork
|
||||
private lateinit var notaryServices: StartedNodeServices
|
||||
private lateinit var aliceNode: StartedMockNode
|
||||
private lateinit var aliceServices: StartedNodeServices
|
||||
private lateinit var notary: Party
|
||||
private lateinit var alice: Party
|
||||
@ -41,7 +53,8 @@ class NotaryServiceTests {
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts"))
|
||||
aliceServices = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)).services
|
||||
aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME))
|
||||
aliceServices = aliceNode.services
|
||||
notaryServices = mockNet.defaultNotaryNode.services //TODO get rid of that
|
||||
notary = mockNet.defaultNotaryIdentity
|
||||
alice = aliceServices.myInfo.singleIdentity()
|
||||
@ -159,6 +172,70 @@ class NotaryServiceTests {
|
||||
notaryError.conflict.verified()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reject when notarisation request not signed by the requesting party`() {
|
||||
runNotarisationAndInterceptClientPayload { originalPayload ->
|
||||
val transaction = originalPayload.signedTransaction
|
||||
val randomKeyPair = Crypto.generateKeyPair()
|
||||
val bytesToSign = NotarisationRequest(transaction.inputs, transaction.id).serialize().bytes
|
||||
val modifiedSignature = NotarisationRequestSignature(randomKeyPair.sign(bytesToSign), aliceServices.myInfo.platformVersion)
|
||||
originalPayload.copy(requestSignature = modifiedSignature)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reject when incorrect notarisation request signed - inputs don't match`() {
|
||||
runNotarisationAndInterceptClientPayload { originalPayload ->
|
||||
val transaction = originalPayload.signedTransaction
|
||||
val wrongInputs = listOf(StateRef(SecureHash.randomSHA256(), 0))
|
||||
val request = NotarisationRequest(wrongInputs, transaction.id)
|
||||
val modifiedSignature = request.generateSignature(aliceServices)
|
||||
originalPayload.copy(requestSignature = modifiedSignature)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reject when incorrect notarisation request signed - transaction id doesn't match`() {
|
||||
runNotarisationAndInterceptClientPayload { originalPayload ->
|
||||
val transaction = originalPayload.signedTransaction
|
||||
val wrongTransactionId = SecureHash.randomSHA256()
|
||||
val request = NotarisationRequest(transaction.inputs, wrongTransactionId)
|
||||
val modifiedSignature = request.generateSignature(aliceServices)
|
||||
originalPayload.copy(requestSignature = modifiedSignature)
|
||||
}
|
||||
}
|
||||
|
||||
private fun runNotarisationAndInterceptClientPayload(payloadModifier: (NotarisationPayload) -> NotarisationPayload) {
|
||||
aliceNode.setMessagingServiceSpy(object : MessagingServiceSpy(aliceNode.network) {
|
||||
override fun send(message: Message, target: MessageRecipients, retryId: Long?, sequenceKey: Any, additionalHeaders: Map<String, String>) {
|
||||
val messageData = message.data.deserialize<Any>() as? InitialSessionMessage
|
||||
val payload = messageData?.firstPayload!!.deserialize()
|
||||
|
||||
if (payload is NotarisationPayload) {
|
||||
val alteredPayload = payloadModifier(payload)
|
||||
val alteredMessageData = messageData.copy(firstPayload = alteredPayload.serialize())
|
||||
val alteredMessage = InMemoryMessagingNetwork.InMemoryMessage(message.topic, OpaqueBytes(alteredMessageData.serialize().bytes), message.uniqueMessageId)
|
||||
messagingService.send(alteredMessage, target, retryId)
|
||||
|
||||
} else {
|
||||
messagingService.send(message, target, retryId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val stx = run {
|
||||
val inputState = issueState(aliceServices, alice)
|
||||
val tx = TransactionBuilder(notary)
|
||||
.addInputState(inputState)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
aliceServices.signInitialTransaction(tx)
|
||||
}
|
||||
|
||||
val future = runNotaryClient(stx)
|
||||
val ex = assertFailsWith(NotaryException::class) { future.getOrThrow() }
|
||||
assertThat(ex.error).isInstanceOf(NotaryError.RequestSignatureInvalid::class.java)
|
||||
}
|
||||
|
||||
private fun runNotaryClient(stx: SignedTransaction): CordaFuture<List<TransactionSignature>> {
|
||||
val flow = NotaryFlow.Client(stx)
|
||||
val future = aliceServices.startFlow(flow)
|
||||
|
@ -4,12 +4,16 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.flows.NotarisationPayload
|
||||
import net.corda.core.flows.NotarisationRequest
|
||||
import net.corda.core.internal.ResolveTransactionsFlow
|
||||
import net.corda.core.internal.validateRequest
|
||||
import net.corda.core.node.AppServiceHub
|
||||
import net.corda.core.node.services.CordaService
|
||||
import net.corda.core.node.services.TimeWindowChecker
|
||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionWithSignatures
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
@ -17,7 +21,8 @@ import java.security.SignatureException
|
||||
/**
|
||||
* A custom notary service should provide a constructor that accepts two parameters of types [AppServiceHub] and [PublicKey].
|
||||
*
|
||||
* Note that at present only a single-node notary service can be customised.
|
||||
* Note that the support for custom notaries is still experimental – at present only a single-node notary service can be customised.
|
||||
* The notary-related APIs might change in the future.
|
||||
*/
|
||||
// START 1
|
||||
@CordaService
|
||||
@ -41,19 +46,15 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
|
||||
@Suspendable
|
||||
override fun receiveAndVerifyTx(): TransactionParts {
|
||||
try {
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false))
|
||||
val stx = receiveTransaction()
|
||||
val notary = stx.notary
|
||||
checkNotary(notary)
|
||||
var timeWindow: TimeWindow? = null
|
||||
val transactionWithSignatures = if (stx.isNotaryChangeTransaction()) {
|
||||
stx.resolveNotaryChangeTransaction(serviceHub)
|
||||
} else {
|
||||
val wtx = stx.tx
|
||||
customVerify(wtx.toLedgerTransaction(serviceHub))
|
||||
timeWindow = wtx.timeWindow
|
||||
stx
|
||||
}
|
||||
checkSignatures(transactionWithSignatures)
|
||||
val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction())
|
||||
null
|
||||
else
|
||||
stx.tx.timeWindow
|
||||
resolveAndContractVerify(stx)
|
||||
verifySignatures(stx)
|
||||
return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!)
|
||||
} catch (e: Exception) {
|
||||
throw when (e) {
|
||||
@ -64,8 +65,25 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
|
||||
}
|
||||
}
|
||||
|
||||
private fun customVerify(transaction: LedgerTransaction) {
|
||||
// Add custom verification logic
|
||||
@Suspendable
|
||||
private fun receiveTransaction(): SignedTransaction {
|
||||
return otherSideSession.receive<NotarisationPayload>().unwrap {
|
||||
val stx = it.signedTransaction
|
||||
validateRequest(NotarisationRequest(stx.inputs, stx.id), it.requestSignature)
|
||||
stx
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun resolveAndContractVerify(stx: SignedTransaction) {
|
||||
subFlow(ResolveTransactionsFlow(stx, otherSideSession))
|
||||
stx.verify(serviceHub, false)
|
||||
customVerify(stx)
|
||||
}
|
||||
|
||||
private fun verifySignatures(stx: SignedTransaction) {
|
||||
val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub)
|
||||
checkSignatures(transactionWithSignatures)
|
||||
}
|
||||
|
||||
private fun checkSignatures(tx: TransactionWithSignatures) {
|
||||
@ -75,5 +93,9 @@ class MyValidatingNotaryFlow(otherSide: FlowSession, service: MyCustomValidating
|
||||
throw NotaryException(NotaryError.TransactionInvalid(e))
|
||||
}
|
||||
}
|
||||
|
||||
private fun customVerify(stx: SignedTransaction) {
|
||||
// Add custom verification logic
|
||||
}
|
||||
}
|
||||
// END 2
|
||||
|
Loading…
Reference in New Issue
Block a user