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:
Andrius Dagys
2018-02-16 16:14:06 +00:00
committed by GitHub
parent fee89c044f
commit 5b93abdc57
13 changed files with 387 additions and 94 deletions

View 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
}

View File

@ -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()
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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")))
}
}

View File

@ -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>