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:
@ -12,13 +12,19 @@ import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.NotaryException
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.NotarisationPayload
|
||||
import net.corda.core.flows.NotarisationRequest
|
||||
import net.corda.core.node.services.NotaryService
|
||||
import net.corda.core.node.services.UniquenessProvider
|
||||
import net.corda.core.schemas.PersistentStateRef
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.FilteredTransaction
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
import net.corda.node.services.config.BFTSMaRtConfiguration
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
@ -67,25 +73,25 @@ class BFTNonValidatingNotaryService(
|
||||
replicaHolder.getOrThrow() // It's enough to wait for the ServiceReplica constructor to return.
|
||||
}
|
||||
|
||||
fun commitTransaction(tx: Any, otherSide: Party) = client.commitTransaction(tx, otherSide)
|
||||
fun commitTransaction(payload: NotarisationPayload, otherSide: Party) = client.commitTransaction(payload, otherSide)
|
||||
|
||||
override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = ServiceFlow(otherPartySession, this)
|
||||
|
||||
private class ServiceFlow(val otherSideSession: FlowSession, val service: BFTNonValidatingNotaryService) : FlowLogic<Void?>() {
|
||||
@Suspendable
|
||||
override fun call(): Void? {
|
||||
val stx = otherSideSession.receive<FilteredTransaction>().unwrap { it }
|
||||
val signatures = commit(stx)
|
||||
val payload = otherSideSession.receive<NotarisationPayload>().unwrap { it }
|
||||
val signatures = commit(payload)
|
||||
otherSideSession.send(signatures)
|
||||
return null
|
||||
}
|
||||
|
||||
private fun commit(stx: FilteredTransaction): List<DigitalSignature> {
|
||||
val response = service.commitTransaction(stx, otherSideSession.counterparty)
|
||||
private fun commit(payload: NotarisationPayload): List<DigitalSignature> {
|
||||
val response = service.commitTransaction(payload, otherSideSession.counterparty)
|
||||
when (response) {
|
||||
is BFTSMaRt.ClusterResponse.Error -> throw NotaryException(response.error)
|
||||
is BFTSMaRt.ClusterResponse.Signatures -> {
|
||||
log.debug("All input states of transaction ${stx.id} have been committed")
|
||||
log.debug("All input states of transaction ${payload.coreTransaction.id} have been committed")
|
||||
return response.txSignatures
|
||||
}
|
||||
}
|
||||
@ -132,28 +138,34 @@ class BFTNonValidatingNotaryService(
|
||||
notaryIdentityKey: PublicKey) : BFTSMaRt.Replica(config, replicaId, createMap, services, notaryIdentityKey) {
|
||||
|
||||
override fun executeCommand(command: ByteArray): ByteArray {
|
||||
val request = command.deserialize<BFTSMaRt.CommitRequest>()
|
||||
val ftx = request.tx as FilteredTransaction
|
||||
val response = verifyAndCommitTx(ftx, request.callerIdentity)
|
||||
val commitRequest = command.deserialize<BFTSMaRt.CommitRequest>()
|
||||
verifyRequest(commitRequest)
|
||||
val response = verifyAndCommitTx(commitRequest.payload.coreTransaction, commitRequest.callerIdentity)
|
||||
return response.serialize().bytes
|
||||
}
|
||||
|
||||
fun verifyAndCommitTx(ftx: FilteredTransaction, callerIdentity: Party): BFTSMaRt.ReplicaResponse {
|
||||
private fun verifyAndCommitTx(transaction: CoreTransaction, callerIdentity: Party): BFTSMaRt.ReplicaResponse {
|
||||
return try {
|
||||
val id = ftx.id
|
||||
val inputs = ftx.inputs
|
||||
val notary = ftx.notary
|
||||
NotaryService.validateTimeWindow(services.clock, ftx.timeWindow)
|
||||
val id = transaction.id
|
||||
val inputs = transaction.inputs
|
||||
val notary = transaction.notary
|
||||
if (transaction is FilteredTransaction) NotaryService.validateTimeWindow(services.clock, transaction.timeWindow)
|
||||
if (notary !in services.myInfo.legalIdentities) throw NotaryException(NotaryError.WrongNotary)
|
||||
commitInputStates(inputs, id, callerIdentity)
|
||||
log.debug { "Inputs committed successfully, signing $id" }
|
||||
BFTSMaRt.ReplicaResponse.Signature(sign(ftx))
|
||||
BFTSMaRt.ReplicaResponse.Signature(sign(id))
|
||||
} catch (e: NotaryException) {
|
||||
log.debug { "Error processing transaction: ${e.error}" }
|
||||
BFTSMaRt.ReplicaResponse.Error(e.error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyRequest(commitRequest: BFTSMaRt.CommitRequest) {
|
||||
val transaction = commitRequest.payload.coreTransaction
|
||||
val notarisationRequest = NotarisationRequest(transaction.inputs, transaction.id)
|
||||
notarisationRequest.verifySignature(commitRequest.payload.requestSignature, commitRequest.callerIdentity)
|
||||
// TODO: persist the signature for traceability.
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
|
@ -17,6 +17,7 @@ import net.corda.core.crypto.*
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.NotaryException
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.NotarisationPayload
|
||||
import net.corda.core.internal.declaredField
|
||||
import net.corda.core.internal.toTypedArray
|
||||
import net.corda.core.node.services.UniquenessProvider
|
||||
@ -25,8 +26,6 @@ import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.FilteredTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
@ -52,7 +51,7 @@ import java.util.*
|
||||
object BFTSMaRt {
|
||||
/** Sent from [Client] to [Replica]. */
|
||||
@CordaSerializable
|
||||
data class CommitRequest(val tx: Any, val callerIdentity: Party)
|
||||
data class CommitRequest(val payload: NotarisationPayload, val callerIdentity: Party)
|
||||
|
||||
/** Sent from [Replica] to [Client]. */
|
||||
@CordaSerializable
|
||||
@ -101,13 +100,12 @@ object BFTSMaRt {
|
||||
* Sends a transaction commit request to the BFT cluster. The [proxy] will deliver the request to every
|
||||
* replica, and block until a sufficient number of replies are received.
|
||||
*/
|
||||
fun commitTransaction(transaction: Any, otherSide: Party): ClusterResponse {
|
||||
require(transaction is FilteredTransaction || transaction is SignedTransaction) { "Unsupported transaction type: ${transaction.javaClass.name}" }
|
||||
fun commitTransaction(payload: NotarisationPayload, otherSide: Party): ClusterResponse {
|
||||
awaitClientConnectionToCluster()
|
||||
cluster.waitUntilAllReplicasHaveInitialized()
|
||||
val requestBytes = CommitRequest(transaction, otherSide).serialize().bytes
|
||||
val requestBytes = CommitRequest(payload, otherSide).serialize().bytes
|
||||
val responseBytes = proxy.invokeOrdered(requestBytes)
|
||||
return responseBytes.deserialize<ClusterResponse>()
|
||||
return responseBytes.deserialize()
|
||||
}
|
||||
|
||||
/** A comparator to check if replies from two replicas are the same. */
|
||||
@ -242,12 +240,15 @@ object BFTSMaRt {
|
||||
}
|
||||
}
|
||||
|
||||
/** Generates a signature over an arbitrary array of bytes. */
|
||||
protected fun sign(bytes: ByteArray): DigitalSignature.WithKey {
|
||||
return services.database.transaction { services.keyManagementService.sign(bytes, notaryIdentityKey) }
|
||||
}
|
||||
|
||||
protected fun sign(filteredTransaction: FilteredTransaction): TransactionSignature {
|
||||
return services.database.transaction { services.createSignature(filteredTransaction, notaryIdentityKey) }
|
||||
/** Generates a transaction signature over the specified transaction [txId]. */
|
||||
protected fun sign(txId: SecureHash): TransactionSignature {
|
||||
val signableData = SignableData(txId, SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(notaryIdentityKey).schemeNumberID))
|
||||
return services.keyManagementService.sign(signableData, notaryIdentityKey)
|
||||
}
|
||||
|
||||
// TODO:
|
||||
|
@ -1,11 +1,15 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.contracts.ComponentGroupEnum
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.NotaryFlow
|
||||
import net.corda.core.flows.TransactionParts
|
||||
import net.corda.core.flows.NotarisationPayload
|
||||
import net.corda.core.flows.NotarisationRequest
|
||||
import net.corda.core.internal.validateRequest
|
||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.FilteredTransaction
|
||||
import net.corda.core.transactions.NotaryChangeWireTransaction
|
||||
import net.corda.core.utilities.unwrap
|
||||
@ -21,22 +25,30 @@ class NonValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAut
|
||||
*/
|
||||
@Suspendable
|
||||
override fun receiveAndVerifyTx(): TransactionParts {
|
||||
val parts = otherSideSession.receive<Any>().unwrap {
|
||||
when (it) {
|
||||
is FilteredTransaction -> {
|
||||
it.verify()
|
||||
it.checkAllComponentsVisible(ComponentGroupEnum.INPUTS_GROUP)
|
||||
it.checkAllComponentsVisible(ComponentGroupEnum.TIMEWINDOW_GROUP)
|
||||
val notary = it.notary
|
||||
TransactionParts(it.id, it.inputs, it.timeWindow, notary)
|
||||
}
|
||||
is NotaryChangeWireTransaction -> TransactionParts(it.id, it.inputs, null, it.notary)
|
||||
else -> {
|
||||
throw IllegalArgumentException("Received unexpected transaction type: ${it::class.java.simpleName}," +
|
||||
"expected either ${FilteredTransaction::class.java.simpleName} or ${NotaryChangeWireTransaction::class.java.simpleName}")
|
||||
return otherSideSession.receive<NotarisationPayload>().unwrap { payload ->
|
||||
val transaction = payload.coreTransaction
|
||||
val request = NotarisationRequest(transaction.inputs, transaction.id)
|
||||
validateRequest(request, payload.requestSignature)
|
||||
extractParts(transaction)
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractParts(tx: CoreTransaction): TransactionParts {
|
||||
return when (tx) {
|
||||
is FilteredTransaction -> {
|
||||
tx.apply {
|
||||
verify()
|
||||
checkAllComponentsVisible(ComponentGroupEnum.INPUTS_GROUP)
|
||||
checkAllComponentsVisible(ComponentGroupEnum.TIMEWINDOW_GROUP)
|
||||
}
|
||||
val notary = tx.notary
|
||||
TransactionParts(tx.id, tx.inputs, tx.timeWindow, notary)
|
||||
}
|
||||
is NotaryChangeWireTransaction -> TransactionParts(tx.id, tx.inputs, null, tx.notary)
|
||||
else -> {
|
||||
throw IllegalArgumentException("Received unexpected transaction type: ${tx::class.java.simpleName}," +
|
||||
"expected either ${FilteredTransaction::class.java.simpleName} or ${NotaryChangeWireTransaction::class.java.simpleName}")
|
||||
}
|
||||
}
|
||||
return parts
|
||||
}
|
||||
}
|
@ -4,8 +4,14 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.contracts.TransactionVerificationException
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.flows.NotarisationPayload
|
||||
import net.corda.core.flows.NotarisationRequest
|
||||
import net.corda.core.internal.ResolveTransactionsFlow
|
||||
import net.corda.core.internal.validateRequest
|
||||
import net.corda.core.node.services.TrustedAuthorityNotaryService
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionWithSignatures
|
||||
import net.corda.core.utilities.unwrap
|
||||
import java.security.SignatureException
|
||||
|
||||
/**
|
||||
@ -22,15 +28,15 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor
|
||||
@Suspendable
|
||||
override fun receiveAndVerifyTx(): TransactionParts {
|
||||
try {
|
||||
val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false))
|
||||
val stx = receiveTransaction()
|
||||
val notary = stx.notary
|
||||
checkNotary(notary)
|
||||
val timeWindow: TimeWindow? = if (stx.isNotaryChangeTransaction())
|
||||
null
|
||||
else
|
||||
stx.tx.timeWindow
|
||||
val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub)
|
||||
checkSignatures(transactionWithSignatures)
|
||||
resolveAndContractVerify(stx)
|
||||
verifySignatures(stx)
|
||||
return TransactionParts(stx.id, stx.inputs, timeWindow, notary!!)
|
||||
} catch (e: Exception) {
|
||||
throw when (e) {
|
||||
@ -41,6 +47,26 @@ class ValidatingNotaryFlow(otherSideSession: FlowSession, service: TrustedAuthor
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun receiveTransaction(): SignedTransaction {
|
||||
return otherSideSession.receive<NotarisationPayload>().unwrap {
|
||||
val stx = it.signedTransaction
|
||||
validateRequest(NotarisationRequest(stx.inputs, stx.id), it.requestSignature)
|
||||
stx
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun resolveAndContractVerify(stx: SignedTransaction) {
|
||||
subFlow(ResolveTransactionsFlow(stx, otherSideSession))
|
||||
stx.verify(serviceHub, false)
|
||||
}
|
||||
|
||||
private fun verifySignatures(stx: SignedTransaction) {
|
||||
val transactionWithSignatures = stx.resolveTransactionWithSignatures(serviceHub)
|
||||
checkSignatures(transactionWithSignatures)
|
||||
}
|
||||
|
||||
private fun checkSignatures(tx: TransactionWithSignatures) {
|
||||
try {
|
||||
tx.verifySignaturesExcept(service.notaryIdentityKey)
|
||||
|
@ -3,24 +3,35 @@ package net.corda.node.services.transactions
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.crypto.sign
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.NotaryException
|
||||
import net.corda.core.flows.NotaryFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.NotarisationPayload
|
||||
import net.corda.core.flows.NotarisationRequest
|
||||
import net.corda.core.flows.NotarisationRequestSignature
|
||||
import net.corda.core.internal.generateSignature
|
||||
import net.corda.core.messaging.MessageRecipients
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.node.services.api.StartedNodeServices
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.node.services.messaging.Message
|
||||
import net.corda.node.services.statemachine.InitialSessionMessage
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.dummyCommand
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockNodeParameters
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.startFlow
|
||||
import net.corda.testing.node.*
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
@ -34,6 +45,7 @@ import kotlin.test.assertTrue
|
||||
class NotaryServiceTests {
|
||||
private lateinit var mockNet: MockNetwork
|
||||
private lateinit var notaryServices: StartedNodeServices
|
||||
private lateinit var aliceNode: StartedMockNode
|
||||
private lateinit var aliceServices: StartedNodeServices
|
||||
private lateinit var notary: Party
|
||||
private lateinit var alice: Party
|
||||
@ -41,7 +53,8 @@ class NotaryServiceTests {
|
||||
@Before
|
||||
fun setup() {
|
||||
mockNet = MockNetwork(cordappPackages = listOf("net.corda.testing.contracts"))
|
||||
aliceServices = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME)).services
|
||||
aliceNode = mockNet.createNode(MockNodeParameters(legalName = ALICE_NAME))
|
||||
aliceServices = aliceNode.services
|
||||
notaryServices = mockNet.defaultNotaryNode.services //TODO get rid of that
|
||||
notary = mockNet.defaultNotaryIdentity
|
||||
alice = aliceServices.myInfo.singleIdentity()
|
||||
@ -159,6 +172,70 @@ class NotaryServiceTests {
|
||||
notaryError.conflict.verified()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reject when notarisation request not signed by the requesting party`() {
|
||||
runNotarisationAndInterceptClientPayload { originalPayload ->
|
||||
val transaction = originalPayload.signedTransaction
|
||||
val randomKeyPair = Crypto.generateKeyPair()
|
||||
val bytesToSign = NotarisationRequest(transaction.inputs, transaction.id).serialize().bytes
|
||||
val modifiedSignature = NotarisationRequestSignature(randomKeyPair.sign(bytesToSign), aliceServices.myInfo.platformVersion)
|
||||
originalPayload.copy(requestSignature = modifiedSignature)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reject when incorrect notarisation request signed - inputs don't match`() {
|
||||
runNotarisationAndInterceptClientPayload { originalPayload ->
|
||||
val transaction = originalPayload.signedTransaction
|
||||
val wrongInputs = listOf(StateRef(SecureHash.randomSHA256(), 0))
|
||||
val request = NotarisationRequest(wrongInputs, transaction.id)
|
||||
val modifiedSignature = request.generateSignature(aliceServices)
|
||||
originalPayload.copy(requestSignature = modifiedSignature)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should reject when incorrect notarisation request signed - transaction id doesn't match`() {
|
||||
runNotarisationAndInterceptClientPayload { originalPayload ->
|
||||
val transaction = originalPayload.signedTransaction
|
||||
val wrongTransactionId = SecureHash.randomSHA256()
|
||||
val request = NotarisationRequest(transaction.inputs, wrongTransactionId)
|
||||
val modifiedSignature = request.generateSignature(aliceServices)
|
||||
originalPayload.copy(requestSignature = modifiedSignature)
|
||||
}
|
||||
}
|
||||
|
||||
private fun runNotarisationAndInterceptClientPayload(payloadModifier: (NotarisationPayload) -> NotarisationPayload) {
|
||||
aliceNode.setMessagingServiceSpy(object : MessagingServiceSpy(aliceNode.network) {
|
||||
override fun send(message: Message, target: MessageRecipients, retryId: Long?, sequenceKey: Any, additionalHeaders: Map<String, String>) {
|
||||
val messageData = message.data.deserialize<Any>() as? InitialSessionMessage
|
||||
val payload = messageData?.firstPayload!!.deserialize()
|
||||
|
||||
if (payload is NotarisationPayload) {
|
||||
val alteredPayload = payloadModifier(payload)
|
||||
val alteredMessageData = messageData.copy(firstPayload = alteredPayload.serialize())
|
||||
val alteredMessage = InMemoryMessagingNetwork.InMemoryMessage(message.topic, OpaqueBytes(alteredMessageData.serialize().bytes), message.uniqueMessageId)
|
||||
messagingService.send(alteredMessage, target, retryId)
|
||||
|
||||
} else {
|
||||
messagingService.send(message, target, retryId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val stx = run {
|
||||
val inputState = issueState(aliceServices, alice)
|
||||
val tx = TransactionBuilder(notary)
|
||||
.addInputState(inputState)
|
||||
.addCommand(dummyCommand(alice.owningKey))
|
||||
aliceServices.signInitialTransaction(tx)
|
||||
}
|
||||
|
||||
val future = runNotaryClient(stx)
|
||||
val ex = assertFailsWith(NotaryException::class) { future.getOrThrow() }
|
||||
assertThat(ex.error).isInstanceOf(NotaryError.RequestSignatureInvalid::class.java)
|
||||
}
|
||||
|
||||
private fun runNotaryClient(stx: SignedTransaction): CordaFuture<List<TransactionSignature>> {
|
||||
val flow = NotaryFlow.Client(stx)
|
||||
val future = aliceServices.startFlow(flow)
|
||||
|
Reference in New Issue
Block a user