Pass a FilteredTransaction instead of a Signed transaction to a non-validating notary flow to preserve privacy.

This also means that the non-validating notary can service requests on the network without loading any custom plugins.
This commit is contained in:
Andrius Dagys 2017-02-08 17:11:00 +00:00
parent 98c30f6432
commit a19dd55257
8 changed files with 117 additions and 70 deletions

View File

@ -5,11 +5,9 @@ import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.contracts.Contract import net.corda.core.contracts.Contract
import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.Party import net.corda.core.crypto.Party
import net.corda.core.messaging.MessageRecipients
import net.corda.core.messaging.MessagingService import net.corda.core.messaging.MessagingService
import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.ServiceEntry
import net.corda.core.randomOrNull import net.corda.core.randomOrNull
import rx.Observable import rx.Observable
@ -73,6 +71,7 @@ interface NetworkMapCache {
/** Look up the node info for a specific peer key. */ /** Look up the node info for a specific peer key. */
fun getNodeByLegalIdentityKey(compositeKey: CompositeKey): NodeInfo? fun getNodeByLegalIdentityKey(compositeKey: CompositeKey): NodeInfo?
/** Look up all nodes advertising the service owned by [compositeKey] */ /** Look up all nodes advertising the service owned by [compositeKey] */
fun getNodesByAdvertisedServiceIdentityKey(compositeKey: CompositeKey): List<NodeInfo> { fun getNodesByAdvertisedServiceIdentityKey(compositeKey: CompositeKey): List<NodeInfo> {
return partyNodes.filter { it.advertisedServices.any { it.identity.owningKey == compositeKey } } return partyNodes.filter { it.advertisedServices.any { it.identity.owningKey == compositeKey } }
@ -108,6 +107,11 @@ interface NetworkMapCache {
/** Checks whether a given party is an advertised notary identity */ /** Checks whether a given party is an advertised notary identity */
fun isNotary(party: Party): Boolean = notaryNodes.any { it.notaryIdentity == party } fun isNotary(party: Party): Boolean = notaryNodes.any { it.notaryIdentity == party }
/** Checks whether a given party is an advertised validating notary identity */
fun isValidatingNotary(party: Party): Boolean {
return notaryNodes.any { it.notaryIdentity == party && it.advertisedServices.any { it.info.type.isValidatingNotary() }}
}
/** /**
* Add a network map service; fetches a copy of the latest map from the service and subscribes to any further * Add a network map service; fetches a copy of the latest map from the service and subscribes to any further
* updates. * updates.

View File

@ -43,6 +43,7 @@ sealed class ServiceType(val id: String) {
fun isSubTypeOf(superType: ServiceType) = (id == superType.id) || id.startsWith(superType.id + ".") fun isSubTypeOf(superType: ServiceType) = (id == superType.id) || id.startsWith(superType.id + ".")
fun isNotary() = isSubTypeOf(notary) fun isNotary() = isSubTypeOf(notary)
fun isValidatingNotary() = isNotary() && id.contains(".validating")
override fun hashCode(): Int = id.hashCode() override fun hashCode(): Int = id.hashCode()
override fun toString(): String = id.toString() override fun toString(): String = id.toString()

View File

@ -0,0 +1,29 @@
package net.corda.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.crypto.Party
import net.corda.core.node.services.TimestampChecker
import net.corda.core.node.services.UniquenessProvider
import net.corda.core.transactions.FilteredTransaction
import net.corda.core.utilities.unwrap
class NonValidatingNotaryFlow(otherSide: Party,
timestampChecker: TimestampChecker,
uniquenessProvider: UniquenessProvider) : NotaryFlow.Service(otherSide, timestampChecker, uniquenessProvider) {
/**
* The received 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
override fun receiveAndVerifyTx(): NotaryFlow.Service.TransactionParts {
val ftx = receive<FilteredTransaction>(otherSide).unwrap {
it.verify()
it
}
return TransactionParts(ftx.rootHash, ftx.filteredLeaves.inputs, ftx.filteredLeaves.timestamp)
}
}

View File

@ -1,10 +1,9 @@
package net.corda.flows package net.corda.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.crypto.DigitalSignature import net.corda.core.contracts.StateRef
import net.corda.core.crypto.Party import net.corda.core.contracts.Timestamp
import net.corda.core.crypto.SignedData import net.corda.core.crypto.*
import net.corda.core.crypto.signWithECDSA
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.node.services.TimestampChecker import net.corda.core.node.services.TimestampChecker
@ -12,13 +11,12 @@ import net.corda.core.node.services.UniquenessException
import net.corda.core.node.services.UniquenessProvider import net.corda.core.node.services.UniquenessProvider
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
object NotaryFlow { object NotaryFlow {
/** /**
* A flow to be used for obtaining a signature from a [NotaryService] ascertaining the transaction * A flow to be used by a party 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. * 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 * @throws NotaryException in case the any of the inputs to the transaction have been consumed
@ -52,8 +50,14 @@ object NotaryFlow {
throw NotaryException(NotaryError.SignaturesMissing(ex)) throw NotaryException(NotaryError.SignaturesMissing(ex))
} }
val payload: Any = if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
stx
} else {
wtx.buildFilteredTransaction { it is StateRef || it is Timestamp }
}
val response = try { val response = try {
sendAndReceive<DigitalSignature.WithKey>(notaryParty, SignRequest(stx)) sendAndReceive<DigitalSignature.WithKey>(notaryParty, payload)
} catch (e: NotaryException) { } catch (e: NotaryException) {
if (e.error is NotaryError.Conflict) { if (e.error is NotaryError.Conflict) {
e.error.conflict.verified() e.error.conflict.verified()
@ -73,64 +77,64 @@ object NotaryFlow {
} }
} }
/** /**
* Checks that the timestamp command is valid (if present) and commits the input state, or returns a conflict * A flow run by a notary service that handles notarisation requests.
*
* It 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. * if any of the input states have been previously committed.
* *
* Extend this class, overriding _beforeCommit_ to add custom transaction processing/validation logic. * Additional transaction validation logic can be added when implementing [receiveAndVerifyTx].
*
* TODO: the notary service should only be able to see timestamp commands and inputs
*/ */
open class Service(val otherSide: Party, abstract class Service(val otherSide: Party,
val timestampChecker: TimestampChecker, val timestampChecker: TimestampChecker,
val uniquenessProvider: UniquenessProvider) : FlowLogic<Unit>() { val uniquenessProvider: UniquenessProvider) : FlowLogic<Unit>() {
@Suspendable @Suspendable
override fun call() { override fun call() {
val stx = receive<SignRequest>(otherSide).unwrap { it.tx } val (id, inputs, timestamp) = receiveAndVerifyTx()
val wtx = stx.tx validateTimestamp(timestamp)
commitInputStates(inputs, id)
signAndSendResponse(id)
}
validateTimestamp(wtx) /**
beforeCommit(stx) * Implement custom logic to receive the transaction to notarise, and perform verification based on validity and
commitInputStates(wtx) * privacy requirements.
val sig = sign(stx.id.bytes) */
@Suspendable
abstract fun receiveAndVerifyTx(): TransactionParts
/**
* The minimum amount of information needed to notarise a transaction. Note that this does not include
* any sensitive transaction details.
*/
data class TransactionParts(val id: SecureHash, val inputs: List<StateRef>, val timestamp: Timestamp?)
@Suspendable
private fun signAndSendResponse(txId: SecureHash) {
val sig = sign(txId.bytes)
send(otherSide, sig) send(otherSide, sig)
} }
private fun validateTimestamp(tx: WireTransaction) { private fun validateTimestamp(t: Timestamp?) {
if (tx.timestamp != null if (t != null && !timestampChecker.isValid(t))
&& !timestampChecker.isValid(tx.timestamp))
throw NotaryException(NotaryError.TimestampInvalid()) 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(stx: SignedTransaction) {
}
/** /**
* A NotaryException is thrown if any of the states have been consumed by a different transaction. Note that * A NotaryException is thrown if any of the states have been consumed by a different transaction. Note that
* this method does not throw an exception when input states are present multiple times within the transaction. * this method does not throw an exception when input states are present multiple times within the transaction.
*/ */
private fun commitInputStates(tx: WireTransaction) { private fun commitInputStates(inputs: List<StateRef>, txId: SecureHash) {
try { try {
uniquenessProvider.commit(tx.inputs, tx.id, otherSide) uniquenessProvider.commit(inputs, txId, otherSide)
} catch (e: UniquenessException) { } catch (e: UniquenessException) {
val conflicts = tx.inputs.filterIndexed { i, stateRef -> val conflicts = inputs.filterIndexed { i, stateRef ->
val consumingTx = e.error.stateHistory[stateRef] val consumingTx = e.error.stateHistory[stateRef]
consumingTx != null && consumingTx != UniquenessProvider.ConsumingTx(tx.id, i, otherSide) consumingTx != null && consumingTx != UniquenessProvider.ConsumingTx(txId, i, otherSide)
} }
if (conflicts.isNotEmpty()) { if (conflicts.isNotEmpty()) {
// TODO: Create a new UniquenessException that only contains the conflicts filtered above. // TODO: Create a new UniquenessException that only contains the conflicts filtered above.
throw notaryException(tx, e) throw notaryException(txId, e)
} }
} }
} }
@ -140,14 +144,12 @@ object NotaryFlow {
return mySigningKey.signWithECDSA(bits) return mySigningKey.signWithECDSA(bits)
} }
private fun notaryException(tx: WireTransaction, e: UniquenessException): NotaryException { private fun notaryException(txId: SecureHash, e: UniquenessException): NotaryException {
val conflictData = e.error.serialize() val conflictData = e.error.serialize()
val signedConflict = SignedData(conflictData, sign(conflictData.bytes)) val signedConflict = SignedData(conflictData, sign(conflictData.bytes))
return NotaryException(NotaryError.Conflict(tx, signedConflict)) return NotaryException(NotaryError.Conflict(txId, signedConflict))
} }
} }
data class SignRequest(val tx: SignedTransaction)
} }
class NotaryException(val error: NotaryError) : FlowException() { class NotaryException(val error: NotaryError) : FlowException() {
@ -155,15 +157,15 @@ class NotaryException(val error: NotaryError) : FlowException() {
} }
sealed class NotaryError { sealed class NotaryError {
class Conflict(val tx: WireTransaction, val conflict: SignedData<UniquenessProvider.Conflict>) : NotaryError() { class Conflict(val txId: SecureHash, val conflict: SignedData<UniquenessProvider.Conflict>) : NotaryError() {
override fun toString() = "One or more input states for transaction ${tx.id} have been used in another transaction" override fun toString() = "One or more input states for transaction $txId have been used in another transaction"
} }
/** Thrown if the time specified in the timestamp command is outside the allowed tolerance */ /** Thrown if the time specified in the timestamp command is outside the allowed tolerance */
class TimestampInvalid : NotaryError() class TimestampInvalid : NotaryError()
class TransactionInvalid(val msg: String) : NotaryError() class TransactionInvalid(val msg: String) : NotaryError()
class SignaturesInvalid(val msg: String): NotaryError() class SignaturesInvalid(val msg: String) : NotaryError()
class SignaturesMissing(val cause: SignedTransaction.SignaturesMissingException) : NotaryError() { class SignaturesMissing(val cause: SignedTransaction.SignaturesMissingException) : NotaryError() {
override fun toString() = cause.toString() override fun toString() = cause.toString()

View File

@ -7,6 +7,7 @@ import net.corda.core.node.services.TimestampChecker
import net.corda.core.node.services.UniquenessProvider import net.corda.core.node.services.UniquenessProvider
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.unwrap
import java.security.SignatureException import java.security.SignatureException
/** /**
@ -19,21 +20,18 @@ class ValidatingNotaryFlow(otherSide: Party,
timestampChecker: TimestampChecker, timestampChecker: TimestampChecker,
uniquenessProvider: UniquenessProvider) : uniquenessProvider: UniquenessProvider) :
NotaryFlow.Service(otherSide, timestampChecker, uniquenessProvider) { NotaryFlow.Service(otherSide, timestampChecker, uniquenessProvider) {
/**
* The received transaction is checked for contract-validity, which requires fully resolving it into a
* [TransactionForVerification], for which the caller also has to to reveal the whole transaction
* dependency chain.
*/
@Suspendable @Suspendable
override fun beforeCommit(stx: SignedTransaction) { override fun receiveAndVerifyTx(): TransactionParts {
try { val stx = receive<SignedTransaction>(otherSide).unwrap { it }
checkSignatures(stx) checkSignatures(stx)
val wtx = stx.tx val wtx = stx.tx
resolveTransaction(wtx) validateTransaction(wtx)
wtx.toLedgerTransaction(serviceHub).verify() return TransactionParts(wtx.id, wtx.inputs, wtx.timestamp)
} catch (e: Exception) {
when (e) {
is TransactionVerificationException -> NotaryException(NotaryError.TransactionInvalid(e.toString()))
is SignatureException -> throw NotaryException(NotaryError.SignaturesInvalid(e.toString()))
else -> throw e
}
}
} }
private fun checkSignatures(stx: SignedTransaction) { private fun checkSignatures(stx: SignedTransaction) {
@ -45,7 +43,19 @@ class ValidatingNotaryFlow(otherSide: Party,
} }
@Suspendable @Suspendable
private fun resolveTransaction(wtx: WireTransaction) { fun validateTransaction(wtx: WireTransaction) {
subFlow(ResolveTransactionsFlow(wtx, otherSide)) try {
resolveTransaction(wtx)
wtx.toLedgerTransaction(serviceHub).verify()
} catch (e: Exception) {
throw when (e) {
is TransactionVerificationException -> NotaryException(NotaryError.TransactionInvalid(e.toString()))
is SignatureException -> NotaryException(NotaryError.SignaturesInvalid(e.toString()))
else -> e
} }
}
}
@Suspendable
private fun resolveTransaction(wtx: WireTransaction) = subFlow(ResolveTransactionsFlow(wtx, otherSide))
} }

View File

@ -53,7 +53,7 @@ class RaftNotaryServiceTests : NodeBasedTest() {
val ex = assertFailsWith(NotaryException::class) { secondSpend.resultFuture.getOrThrow() } val ex = assertFailsWith(NotaryException::class) { secondSpend.resultFuture.getOrThrow() }
val error = ex.error as NotaryError.Conflict val error = ex.error as NotaryError.Conflict
assertEquals(error.tx, secondSpendTx.tx) assertEquals(error.txId, secondSpendTx.id)
} }
private fun issueState(node: AbstractNode, notary: Party, notaryKey: KeyPair): StateAndRef<*> { private fun issueState(node: AbstractNode, notary: Party, notaryKey: KeyPair): StateAndRef<*> {

View File

@ -4,6 +4,7 @@ import net.corda.core.crypto.Party
import net.corda.core.node.services.ServiceType import net.corda.core.node.services.ServiceType
import net.corda.core.node.services.TimestampChecker import net.corda.core.node.services.TimestampChecker
import net.corda.core.node.services.UniquenessProvider import net.corda.core.node.services.UniquenessProvider
import net.corda.flows.NonValidatingNotaryFlow
import net.corda.flows.NotaryFlow import net.corda.flows.NotaryFlow
import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.ServiceHubInternal
@ -16,6 +17,6 @@ class SimpleNotaryService(services: ServiceHubInternal,
} }
override fun createFlow(otherParty: Party): NotaryFlow.Service { override fun createFlow(otherParty: Party): NotaryFlow.Service {
return NotaryFlow.Service(otherParty, timestampChecker, uniquenessProvider) return NonValidatingNotaryFlow(otherParty, timestampChecker, uniquenessProvider)
} }
} }

View File

@ -128,7 +128,7 @@ class NotaryServiceTests {
val ex = assertFailsWith(NotaryException::class) { future.resultFuture.getOrThrow() } val ex = assertFailsWith(NotaryException::class) { future.resultFuture.getOrThrow() }
val notaryError = ex.error as NotaryError.Conflict val notaryError = ex.error as NotaryError.Conflict
assertEquals(notaryError.tx, stx2.tx) assertEquals(notaryError.txId, stx2.id)
notaryError.conflict.verified() notaryError.conflict.verified()
} }