mirror of
https://github.com/corda/corda.git
synced 2025-06-18 23:28:21 +00:00
Split up Notary protocol into Client and Service parts. The Service protocol can be extended to provide additional transaction processing logic, e.g. validation.
Implemented a Simple and Validating Notary services.
This commit is contained in:
@ -1,11 +1,3 @@
|
||||
/*
|
||||
* Copyright 2016 Distributed Ledger Group LLC. Distributed as Licensed Company IP to DLG Group Members
|
||||
* pursuant to the August 7, 2015 Advisory Services Agreement and subject to the Company IP License terms
|
||||
* set forth therein.
|
||||
*
|
||||
* All other rights reserved.
|
||||
*/
|
||||
|
||||
package com.r3corda.core.node.services
|
||||
|
||||
/**
|
||||
@ -23,11 +15,13 @@ abstract class ServiceType(val id: String) {
|
||||
}
|
||||
|
||||
override operator fun equals(other: Any?): Boolean =
|
||||
if (other is ServiceType) {
|
||||
id == other.id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
if (other is ServiceType) {
|
||||
id == other.id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
fun isSubTypeOf(superType: ServiceType) = (id == superType.id) || id.startsWith(superType.id + ".")
|
||||
|
||||
override fun hashCode(): Int = id.hashCode()
|
||||
override fun toString(): String = id.toString()
|
||||
|
@ -0,0 +1,26 @@
|
||||
package com.r3corda.core.node.services
|
||||
|
||||
import com.r3corda.core.contracts.TimestampCommand
|
||||
import com.r3corda.core.seconds
|
||||
import com.r3corda.core.until
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
|
||||
/**
|
||||
* Checks if the given timestamp falls within the allowed tolerance interval
|
||||
*/
|
||||
class TimestampChecker(val clock: Clock = Clock.systemUTC(),
|
||||
val tolerance: Duration = 30.seconds) {
|
||||
fun isValid(timestampCommand: TimestampCommand): Boolean {
|
||||
val before = timestampCommand.before
|
||||
val after = timestampCommand.after
|
||||
|
||||
val now = clock.instant()
|
||||
|
||||
// We don't need to test for (before == null && after == null) or backwards bounds because the TimestampCommand
|
||||
// constructor already checks that.
|
||||
if (before != null && before until now > tolerance) return false
|
||||
if (after != null && now until after > tolerance) return false
|
||||
return true
|
||||
}
|
||||
}
|
@ -1,95 +1,191 @@
|
||||
package com.r3corda.protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.contracts.TimestampCommand
|
||||
import com.r3corda.core.contracts.WireTransaction
|
||||
import com.r3corda.core.crypto.DigitalSignature
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SignedData
|
||||
import com.r3corda.core.crypto.signWithECDSA
|
||||
import com.r3corda.core.messaging.SingleMessageRecipient
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.node.services.TimestampChecker
|
||||
import com.r3corda.core.node.services.UniquenessException
|
||||
import com.r3corda.core.node.services.UniquenessProvider
|
||||
import com.r3corda.core.noneOrSingle
|
||||
import com.r3corda.core.protocols.ProtocolLogic
|
||||
import com.r3corda.core.random63BitValue
|
||||
import com.r3corda.core.serialization.SerializedBytes
|
||||
import com.r3corda.core.serialization.deserialize
|
||||
import com.r3corda.core.serialization.serialize
|
||||
import com.r3corda.core.utilities.ProgressTracker
|
||||
import com.r3corda.core.utilities.UntrustworthyData
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
* A protocol to be used for obtaining a signature from a [NotaryService] ascertaining the transaction
|
||||
* timestamp is correct and none of its inputs have been used in another completed transaction
|
||||
*
|
||||
* @throws NotaryException in case the any of the inputs to the transaction have been consumed
|
||||
* by another transaction or the timestamp is invalid
|
||||
*/
|
||||
class NotaryProtocol(private val wtx: WireTransaction,
|
||||
override val progressTracker: ProgressTracker = NotaryProtocol.tracker()) : ProtocolLogic<DigitalSignature.LegallyIdentifiable>() {
|
||||
companion object {
|
||||
val TOPIC = "platform.notary.request"
|
||||
object NotaryProtocol {
|
||||
val TOPIC = "platform.notary.request"
|
||||
val TOPIC_INITIATE = "platform.notary.initiate"
|
||||
|
||||
object REQUESTING : ProgressTracker.Step("Requesting signature by Notary service")
|
||||
/**
|
||||
* A protocol to be used for obtaining a signature from a [NotaryService] ascertaining the transaction
|
||||
* timestamp is correct and none of its inputs have been used in another completed transaction
|
||||
*
|
||||
* @throws NotaryException in case the any of the inputs to the transaction have been consumed
|
||||
* by another transaction or the timestamp is invalid
|
||||
*/
|
||||
class Client(private val wtx: WireTransaction,
|
||||
override val progressTracker: ProgressTracker = Client.tracker()) : ProtocolLogic<DigitalSignature.LegallyIdentifiable>() {
|
||||
companion object {
|
||||
|
||||
object VALIDATING : ProgressTracker.Step("Validating response from Notary service")
|
||||
object REQUESTING : ProgressTracker.Step("Requesting signature by Notary service")
|
||||
|
||||
fun tracker() = ProgressTracker(REQUESTING, VALIDATING)
|
||||
}
|
||||
object VALIDATING : ProgressTracker.Step("Validating response from Notary service")
|
||||
|
||||
lateinit var notaryNode: NodeInfo
|
||||
fun tracker() = ProgressTracker(REQUESTING, VALIDATING)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): DigitalSignature.LegallyIdentifiable {
|
||||
progressTracker.currentStep = REQUESTING
|
||||
notaryNode = findNotaryNode()
|
||||
lateinit var notaryNode: NodeInfo
|
||||
|
||||
val sessionID = random63BitValue()
|
||||
val request = SignRequest(wtx.serialized, serviceHub.storageService.myLegalIdentity, serviceHub.networkService.myAddress, sessionID)
|
||||
val response = sendAndReceive<Result>(TOPIC, notaryNode.address, 0, sessionID, request)
|
||||
@Suspendable
|
||||
override fun call(): DigitalSignature.LegallyIdentifiable {
|
||||
progressTracker.currentStep = REQUESTING
|
||||
notaryNode = findNotaryNode()
|
||||
|
||||
val notaryResult = validateResponse(response)
|
||||
return notaryResult.sig ?: throw NotaryException(notaryResult.error!!)
|
||||
}
|
||||
val sendSessionID = random63BitValue()
|
||||
val receiveSessionID = random63BitValue()
|
||||
|
||||
private fun validateResponse(response: UntrustworthyData<Result>): Result {
|
||||
progressTracker.currentStep = VALIDATING
|
||||
val handshake = Handshake(serviceHub.networkService.myAddress, sendSessionID, receiveSessionID)
|
||||
sendAndReceive<Unit>(TOPIC_INITIATE, notaryNode.address, 0, receiveSessionID, handshake)
|
||||
|
||||
response.validate {
|
||||
if (it.sig != null) validateSignature(it.sig, wtx.serialized)
|
||||
else if (it.error is NotaryError.Conflict) it.error.conflict.verified()
|
||||
else if (it.error == null || it.error !is NotaryError)
|
||||
throw IllegalStateException("Received invalid result from Notary service '${notaryNode.identity}'")
|
||||
return it
|
||||
val request = SignRequest(wtx.serialized, serviceHub.storageService.myLegalIdentity)
|
||||
val response = sendAndReceive<Result>(TOPIC, notaryNode.address, sendSessionID, receiveSessionID, request)
|
||||
|
||||
val notaryResult = validateResponse(response)
|
||||
return notaryResult.sig ?: throw NotaryException(notaryResult.error!!)
|
||||
}
|
||||
|
||||
private fun validateResponse(response: UntrustworthyData<Result>): Result {
|
||||
progressTracker.currentStep = VALIDATING
|
||||
|
||||
response.validate {
|
||||
if (it.sig != null) validateSignature(it.sig, wtx.serialized)
|
||||
else if (it.error is NotaryError.Conflict) it.error.conflict.verified()
|
||||
else if (it.error == null || it.error !is NotaryError)
|
||||
throw IllegalStateException("Received invalid result from Notary service '${notaryNode.identity}'")
|
||||
return it
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateSignature(sig: DigitalSignature.LegallyIdentifiable, data: SerializedBytes<WireTransaction>) {
|
||||
check(sig.signer == notaryNode.identity) { "Notary result not signed by the correct service" }
|
||||
sig.verifyWithECDSA(data)
|
||||
}
|
||||
|
||||
private fun findNotaryNode(): NodeInfo {
|
||||
var maybeNotaryKey: PublicKey? = null
|
||||
|
||||
val timestampCommand = wtx.commands.singleOrNull { it.value is TimestampCommand }
|
||||
if (timestampCommand != null) maybeNotaryKey = timestampCommand.signers.first()
|
||||
|
||||
for (stateRef in wtx.inputs) {
|
||||
val inputNotaryKey = serviceHub.loadState(stateRef).notary.owningKey
|
||||
if (maybeNotaryKey != null)
|
||||
check(maybeNotaryKey == inputNotaryKey) { "Input states and timestamp must have the same Notary" }
|
||||
else maybeNotaryKey = inputNotaryKey
|
||||
}
|
||||
|
||||
val notaryKey = maybeNotaryKey ?: throw IllegalStateException("Transaction does not specify a Notary")
|
||||
val notaryNode = serviceHub.networkMapCache.getNodeByPublicKey(notaryKey)
|
||||
return notaryNode ?: throw IllegalStateException("No Notary node can be found with the specified public key")
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateSignature(sig: DigitalSignature.LegallyIdentifiable, data: SerializedBytes<WireTransaction>) {
|
||||
check(sig.signer == notaryNode.identity) { "Notary result not signed by the correct service" }
|
||||
sig.verifyWithECDSA(data)
|
||||
}
|
||||
/**
|
||||
* Checks that the timestamp command is valid (if present) and commits the input state, or returns a conflict
|
||||
* if any of the input states have been previously committed.
|
||||
*
|
||||
* Extend this class, overriding _beforeCommit_ to add custom transaction processing/validation logic.
|
||||
*
|
||||
* TODO: the notary service should only be able to see timestamp commands and inputs
|
||||
*/
|
||||
open class Service(val otherSide: SingleMessageRecipient,
|
||||
val sendSessionID: Long,
|
||||
val receiveSessionID: Long,
|
||||
val timestampChecker: TimestampChecker,
|
||||
val uniquenessProvider: UniquenessProvider) : ProtocolLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val request = receive<SignRequest>(TOPIC, receiveSessionID).validate { it }
|
||||
val txBits = request.txBits
|
||||
val reqIdentity = request.callerIdentity
|
||||
|
||||
private fun findNotaryNode(): NodeInfo {
|
||||
var maybeNotaryKey: PublicKey? = null
|
||||
val wtx = txBits.deserialize()
|
||||
val result: Result
|
||||
try {
|
||||
validateTimestamp(wtx)
|
||||
beforeCommit(wtx, reqIdentity)
|
||||
commitInputStates(wtx, reqIdentity)
|
||||
|
||||
val timestampCommand = wtx.commands.singleOrNull { it.value is TimestampCommand }
|
||||
if (timestampCommand != null) maybeNotaryKey = timestampCommand.signers.first()
|
||||
val sig = sign(txBits)
|
||||
result = Result.noError(sig)
|
||||
|
||||
for (stateRef in wtx.inputs) {
|
||||
val inputNotaryKey = serviceHub.loadState(stateRef).notary.owningKey
|
||||
if (maybeNotaryKey != null)
|
||||
check(maybeNotaryKey == inputNotaryKey) { "Input states and timestamp must have the same Notary" }
|
||||
else maybeNotaryKey = inputNotaryKey
|
||||
} catch(e: NotaryException) {
|
||||
result = Result.withError(e.error)
|
||||
}
|
||||
|
||||
send(TOPIC, otherSide, sendSessionID, result)
|
||||
}
|
||||
|
||||
val notaryKey = maybeNotaryKey ?: throw IllegalStateException("Transaction does not specify a Notary")
|
||||
val notaryNode = serviceHub.networkMapCache.getNodeByPublicKey(notaryKey)
|
||||
return notaryNode ?: throw IllegalStateException("No Notary node can be found with the specified public key")
|
||||
private fun validateTimestamp(tx: WireTransaction) {
|
||||
val timestampCmd = try {
|
||||
tx.commands.noneOrSingle { it.value is TimestampCommand } ?: return
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw NotaryException(NotaryError.MoreThanOneTimestamp())
|
||||
}
|
||||
val myIdentity = serviceHub.storageService.myLegalIdentity
|
||||
if (!timestampCmd.signers.contains(myIdentity.owningKey))
|
||||
throw NotaryException(NotaryError.NotForMe())
|
||||
if (!timestampChecker.isValid(timestampCmd.value as TimestampCommand))
|
||||
throw NotaryException(NotaryError.TimestampInvalid())
|
||||
}
|
||||
|
||||
/**
|
||||
* No pre-commit processing is done. Transaction is not checked for contract-validity, as that would require fully
|
||||
* resolving it into a [TransactionForVerification], for which the caller would have to reveal the whole transaction
|
||||
* history chain.
|
||||
* As a result, the Notary _will commit invalid transactions_ as well, but as it also records the identity of
|
||||
* the caller, it is possible to raise a dispute and verify the validity of the transaction and subsequently
|
||||
* undo the commit of the input states (the exact mechanism still needs to be worked out)
|
||||
*/
|
||||
@Suspendable
|
||||
open fun beforeCommit(wtx: WireTransaction, reqIdentity: Party) {
|
||||
}
|
||||
|
||||
private fun commitInputStates(tx: WireTransaction, reqIdentity: Party) {
|
||||
try {
|
||||
uniquenessProvider.commit(tx, reqIdentity)
|
||||
} catch (e: UniquenessException) {
|
||||
val conflictData = e.error.serialize()
|
||||
val signedConflict = SignedData(conflictData, sign(conflictData))
|
||||
throw NotaryException(NotaryError.Conflict(tx, signedConflict))
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Any> sign(bits: SerializedBytes<T>): DigitalSignature.LegallyIdentifiable {
|
||||
val mySigningKey = serviceHub.storageService.myLegalIdentityKey
|
||||
val myIdentity = serviceHub.storageService.myLegalIdentity
|
||||
return mySigningKey.signWithECDSA(bits, myIdentity)
|
||||
}
|
||||
}
|
||||
|
||||
class Handshake(
|
||||
replyTo: SingleMessageRecipient,
|
||||
val sendSessionID: Long,
|
||||
sessionID: Long) : AbstractRequestMessage(replyTo, sessionID)
|
||||
|
||||
/** TODO: The caller must authenticate instead of just specifying its identity */
|
||||
class SignRequest(val txBits: SerializedBytes<WireTransaction>,
|
||||
val callerIdentity: Party,
|
||||
replyTo: SingleMessageRecipient,
|
||||
sessionID: Long) : AbstractRequestMessage(replyTo, sessionID)
|
||||
val callerIdentity: Party)
|
||||
|
||||
data class Result private constructor(val sig: DigitalSignature.LegallyIdentifiable?, val error: NotaryError?) {
|
||||
companion object {
|
||||
@ -97,6 +193,24 @@ class NotaryProtocol(private val wtx: WireTransaction,
|
||||
fun noError(sig: DigitalSignature.LegallyIdentifiable) = Result(sig, null)
|
||||
}
|
||||
}
|
||||
|
||||
interface Factory {
|
||||
fun create(otherSide: SingleMessageRecipient,
|
||||
sendSessionID: Long,
|
||||
receiveSessionID: Long,
|
||||
timestampChecker: TimestampChecker,
|
||||
uniquenessProvider: UniquenessProvider): Service
|
||||
}
|
||||
|
||||
object DefaultFactory : Factory {
|
||||
override fun create(otherSide: SingleMessageRecipient,
|
||||
sendSessionID: Long,
|
||||
receiveSessionID: Long,
|
||||
timestampChecker: TimestampChecker,
|
||||
uniquenessProvider: UniquenessProvider): Service {
|
||||
return Service(otherSide, sendSessionID, receiveSessionID, timestampChecker, uniquenessProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NotaryException(val error: NotaryError) : Exception() {
|
||||
@ -115,4 +229,6 @@ sealed class NotaryError {
|
||||
|
||||
/** Thrown if the time specified in the timestamp command is outside the allowed tolerance */
|
||||
class TimestampInvalid : NotaryError()
|
||||
|
||||
class TransactionInvalid : NotaryError()
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package com.r3corda.protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.r3corda.core.*
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.messaging.SingleMessageRecipient
|
||||
|
@ -158,7 +158,7 @@ object TwoPartyDealProtocol {
|
||||
@Suspendable
|
||||
private fun getNotarySignature(stx: SignedTransaction): DigitalSignature.LegallyIdentifiable {
|
||||
progressTracker.currentStep = NOTARY
|
||||
return subProtocol(NotaryProtocol(stx.tx))
|
||||
return subProtocol(NotaryProtocol.Client(stx.tx))
|
||||
}
|
||||
|
||||
open fun signWithOurKey(partialTX: SignedTransaction): DigitalSignature.WithKey {
|
||||
|
@ -0,0 +1,48 @@
|
||||
package com.r3corda.protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.r3corda.core.contracts.TransactionVerificationException
|
||||
import com.r3corda.core.contracts.WireTransaction
|
||||
import com.r3corda.core.contracts.toLedgerTransaction
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.messaging.SingleMessageRecipient
|
||||
import com.r3corda.core.node.services.TimestampChecker
|
||||
import com.r3corda.core.node.services.UniquenessProvider
|
||||
import java.security.SignatureException
|
||||
|
||||
/**
|
||||
* A notary commit protocol that makes sure a given transaction is valid before committing it. This does mean that the calling
|
||||
* party has to reveal the whole transaction history; however, we avoid complex conflict resolution logic where a party
|
||||
* has its input states "blocked" by a transaction from another party, and needs to establish whether that transaction was
|
||||
* indeed valid
|
||||
*/
|
||||
class ValidatingNotaryProtocol(otherSide: SingleMessageRecipient,
|
||||
sessionIdForSend: Long,
|
||||
sessionIdForReceive: Long,
|
||||
timestampChecker: TimestampChecker,
|
||||
uniquenessProvider: UniquenessProvider) : NotaryProtocol.Service(otherSide, sessionIdForSend, sessionIdForReceive, timestampChecker, uniquenessProvider) {
|
||||
@Suspendable
|
||||
override fun beforeCommit(wtx: WireTransaction, reqIdentity: Party) {
|
||||
try {
|
||||
validateDependencies(reqIdentity, wtx)
|
||||
checkContractValid(wtx)
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is TransactionVerificationException,
|
||||
is SignatureException -> throw NotaryException(NotaryError.TransactionInvalid())
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkContractValid(wtx: WireTransaction) {
|
||||
val ltx = wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
|
||||
serviceHub.verifyTransaction(ltx)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun validateDependencies(reqIdentity: Party, wtx: WireTransaction) {
|
||||
val otherSide = serviceHub.networkMapCache.getNodeByPublicKey(reqIdentity.owningKey)!!.address
|
||||
subProtocol(ResolveTransactionsProtocol(wtx, otherSide))
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user