mirror of
https://github.com/corda/corda.git
synced 2025-06-17 14:48:16 +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:
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,16 +124,16 @@ 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.
|
||||
*/
|
||||
private class SendTransactionWithRetry(otherSideSession: FlowSession, stx: SignedTransaction) : SendTransactionFlow(otherSideSession, stx) {
|
||||
@Suspendable
|
||||
override fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any): UntrustworthyData<FetchDataFlow.Request> {
|
||||
return otherSideSession.sendAndReceiveWithRetry(payload)
|
||||
/**
|
||||
* The [NotarySendTransactionFlow] flow is similar to [SendTransactionFlow], but uses [NotarisationPayload] as the
|
||||
* initial message, and retries message delivery.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
@ -381,4 +385,20 @@ 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>
|
||||
|
Reference in New Issue
Block a user