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:
Andrius Dagys
2016-05-24 18:01:30 +01:00
parent 654dc3f60a
commit c45bc0df20
21 changed files with 453 additions and 203 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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