From 95b80b37b78127623bdf349749214968d2d96e6d Mon Sep 17 00:00:00 2001 From: "adam.houston" Date: Tue, 15 Mar 2022 11:53:38 +0000 Subject: [PATCH] Enclave tests working --- .../encryptedtx/enclave/EncryptedTxEnclave.kt | 137 ++++++-- .../encryptedtx/EncryptedTxEnclaveTest.kt | 314 ++++++++++++++++-- .../core/conclave/common/EnclaveClient.kt | 10 +- .../EncryptedVerifiableTxAndDependencies.kt | 12 + .../corda/core/flows/CollectSignaturesFlow.kt | 57 ++-- .../core/flows/ReceiveTransactionFlow.kt | 42 ++- .../corda/core/flows/SendTransactionFlow.kt | 4 +- .../kotlin/net/corda/core/node/ServiceHub.kt | 8 + .../services/EncryptedTransactionService.kt | 11 +- .../core/transactions/SignedTransaction.kt | 4 +- .../net/corda/node/internal/AbstractNode.kt | 17 +- .../node/services/DbTransactionsResolver.kt | 14 +- .../node/services/api/ServiceHubInternal.kt | 22 ++ .../encryptedtx/EncryptedBackchainTests.kt | 304 ----------------- .../net/corda/testing/node/MockServices.kt | 7 + 15 files changed, 534 insertions(+), 429 deletions(-) create mode 100644 core/src/main/kotlin/net/corda/core/conclave/common/dto/EncryptedVerifiableTxAndDependencies.kt delete mode 100644 node/src/test/kotlin/net/corda/node/services/encryptedtx/EncryptedBackchainTests.kt diff --git a/common/mock-enclave/src/main/kotlin/com/r3/conclave/encryptedtx/enclave/EncryptedTxEnclave.kt b/common/mock-enclave/src/main/kotlin/com/r3/conclave/encryptedtx/enclave/EncryptedTxEnclave.kt index 774e9c64c2..c8dda45412 100644 --- a/common/mock-enclave/src/main/kotlin/com/r3/conclave/encryptedtx/enclave/EncryptedTxEnclave.kt +++ b/common/mock-enclave/src/main/kotlin/com/r3/conclave/encryptedtx/enclave/EncryptedTxEnclave.kt @@ -1,14 +1,24 @@ package com.r3.conclave.encryptedtx.enclave import com.github.benmanes.caffeine.cache.Caffeine +import net.corda.core.conclave.common.EnclaveClient import net.corda.core.conclave.common.LedgerTxHelper import net.corda.core.conclave.common.dto.ConclaveLedgerTxModel +import net.corda.core.conclave.common.dto.EncryptedVerifiableTxAndDependencies import net.corda.core.conclave.common.dto.VerifiableTxAndDependencies import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata +import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.sign import net.corda.core.internal.dependencies +import net.corda.core.node.AppServiceHub +import net.corda.core.node.ServiceHub +import net.corda.core.node.services.CordaService +import net.corda.core.serialization.ConstructorForDeserialization +import net.corda.core.serialization.SerializeAsToken +import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.EncryptedTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ByteSequence @@ -18,8 +28,9 @@ import net.corda.serialization.internal.SerializationFactoryImpl import net.corda.serialization.internal.amqp.SerializationFactoryCacheKey import net.corda.serialization.internal.amqp.SerializerFactory -class EncryptedTxEnclave { +class EncryptedTxEnclaveClient() : EnclaveClient { + // this will be 'our' key to sign over verified transactions private val enclaveKeyPair = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256") private val signatureMetadata = SignatureMetadata( platformVersion = 1, @@ -43,37 +54,71 @@ class EncryptedTxEnclave { } } - fun encryptSignedTx(ledgerTxBytes: ByteArray): EncryptedTransaction { - val ledgerTxModel = serializationFactoryImpl.deserialize( - byteSequence = ByteSequence.of(ledgerTxBytes, 0, ledgerTxBytes.size), - clazz = ConclaveLedgerTxModel::class.java, - context = AMQP_P2P_CONTEXT - ) + override fun getEnclaveInstanceInfo(): ByteArray { - val signedTransaction = ledgerTxModel.signedTransaction - val signableData = SignableData(signedTransaction.id, signatureMetadata) - val transactionSignature = enclaveKeyPair.sign(signableData) - - return EncryptedTransaction( - id = signedTransaction.id, - encryptedBytes = ledgerTxBytes, - dependencies = signedTransaction.dependencies, - sigs = listOf(transactionSignature) - ) + // no remote attestations in this remote enclave + return byteArrayOf() } - fun verifyTx(txAndDependenciesBytes: ByteArray) { - val txAndDependencies = serializationFactoryImpl.deserialize( - byteSequence = ByteSequence.of(txAndDependenciesBytes, 0, txAndDependenciesBytes.size), - clazz = VerifiableTxAndDependencies::class.java, - context = AMQP_P2P_CONTEXT) + override fun enclaveVerifyAndEncrypt(txAndDependencies: VerifiableTxAndDependencies, checkSufficientSignatures: Boolean): EncryptedTransaction { + + verifyTx(txAndDependencies, checkSufficientSignatures) + + val ledgerTx = txAndDependencies.conclaveLedgerTxModel + val transactionSignature = getSignature(ledgerTx.signedTransaction.id) + return encrypt(ledgerTx).addSignature(transactionSignature) + } + + override fun encryptTransactionForLocal(encryptedTransaction: EncryptedTransaction): EncryptedTransaction { + // no re-encryption in this mock enclave, in a real one we'd need to decrypt from the remote then re-encrypt with whatever key + // we want to use for long term storage + return encryptedTransaction + } + + override fun enclaveVerify(encryptedTxAndDependencies: EncryptedVerifiableTxAndDependencies): EncryptedTransaction { + val decrypted = decrypt(encryptedTxAndDependencies.encryptedTransaction) + + val verifiableTxAndDependencies = VerifiableTxAndDependencies( + decrypted, + encryptedTxAndDependencies.dependencies, + encryptedTxAndDependencies.encryptedDependencies + ) + + verifyTx(verifiableTxAndDependencies) + + val transactionSignature = getSignature(decrypted.signedTransaction.id) + return encrypt(decrypted).addSignature(transactionSignature) + } + + override fun encryptTransactionForRemote(conclaveLedgerTxModel: ConclaveLedgerTxModel, remoteAttestation: ByteArray): EncryptedTransaction { + // just serialise in this mock enclave, in a real one we'd need to encrypt for the remote party + return encrypt(conclaveLedgerTxModel) + } + + override fun encryptTransactionForRemote(encryptedTransaction: EncryptedTransaction, remoteAttestation: ByteArray): EncryptedTransaction { + + // no re-encryption in this mock enclave, in a real one we'd need to decrypt from the remote then re-encrypt with whatever key + // we want to use for long term storage + return encryptedTransaction + } + + private fun getSignature(transactionId : SecureHash) : TransactionSignature { + val signableData = SignableData(transactionId, signatureMetadata) + return enclaveKeyPair.sign(signableData) + } + + private fun verifyTx(txAndDependencies: VerifiableTxAndDependencies, checkSufficientSignatures: Boolean = true) { val signedTransaction = txAndDependencies.conclaveLedgerTxModel.signedTransaction - signedTransaction.verifyRequiredSignatures() - val dependencies = decryptDependencies(txAndDependencies.encryptedDependencies) - dependencies.forEach { - it.verifyRequiredSignatures() + if (checkSufficientSignatures) { + signedTransaction.verifyRequiredSignatures() + } + + val dependencies = txAndDependencies.dependencies + decryptDependencies(txAndDependencies.encryptedDependencies) + + require(dependencies.map { it.id }.containsAll(signedTransaction.dependencies)) { + "Missing dependencies to resolve transaction" } val ledgerTransaction = LedgerTxHelper.toLedgerTxInternal(txAndDependencies.conclaveLedgerTxModel, dependencies) @@ -83,12 +128,40 @@ class EncryptedTxEnclave { private fun decryptDependencies(dependencies: Set): Set { // simply deserialize for this "mock enclave" return dependencies.map { - val conclaveLedgerTxModel = serializationFactoryImpl.deserialize( - byteSequence = ByteSequence.of(it.encryptedBytes, 0, it.encryptedBytes.size), - clazz = ConclaveLedgerTxModel::class.java, - context = AMQP_P2P_CONTEXT) + dependency -> - conclaveLedgerTxModel.signedTransaction + // firstly, ensure that WE have signed over this dependency before, else we cannot trust that it has been verified + val ourSig = dependency.sigs.singleOrNull { it.by == enclaveKeyPair.public } + + ourSig?.let { + it.verify(dependency.id) + } ?: throw IllegalStateException("An encrypted dependency was provided ") + + decrypt(dependency).signedTransaction }.toSet() } -} \ No newline at end of file + + private fun encrypt(ledgerTx: ConclaveLedgerTxModel): EncryptedTransaction { + + return EncryptedTransaction( + ledgerTx.signedTransaction.id, + serializationFactoryImpl.serialize( + obj = ledgerTx, + context = AMQP_P2P_CONTEXT).bytes, + ledgerTx.inputStates.map { it.ref.txhash }.toSet(), + emptyList() + ) + } + + private fun EncryptedTransaction.addSignature(extraSignature : TransactionSignature) : EncryptedTransaction { + return this.copy(sigs = this.sigs + extraSignature) + } + + private fun decrypt(encryptedTransaction: EncryptedTransaction): ConclaveLedgerTxModel { + + return serializationFactoryImpl.deserialize( + byteSequence = ByteSequence.of(encryptedTransaction.encryptedBytes, 0, encryptedTransaction.encryptedBytes.size), + clazz = ConclaveLedgerTxModel::class.java, + context = AMQP_P2P_CONTEXT) + } +} diff --git a/common/mock-enclave/src/test/kotlin/com/r3/conclave/encryptedtx/EncryptedTxEnclaveTest.kt b/common/mock-enclave/src/test/kotlin/com/r3/conclave/encryptedtx/EncryptedTxEnclaveTest.kt index aa628d5654..6e10a45b48 100644 --- a/common/mock-enclave/src/test/kotlin/com/r3/conclave/encryptedtx/EncryptedTxEnclaveTest.kt +++ b/common/mock-enclave/src/test/kotlin/com/r3/conclave/encryptedtx/EncryptedTxEnclaveTest.kt @@ -1,15 +1,36 @@ package com.r3.conclave.encryptedtx +import co.paralleluniverse.fibers.Suspendable import com.github.benmanes.caffeine.cache.Caffeine -import com.r3.conclave.encryptedtx.enclave.EncryptedTxEnclave +import com.r3.conclave.encryptedtx.enclave.EncryptedTxEnclaveClient import net.corda.core.conclave.common.dto.ConclaveLedgerTxModel import net.corda.core.conclave.common.dto.VerifiableTxAndDependencies +import net.corda.core.contracts.BelongsToContract +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.Contract +import net.corda.core.contracts.ContractState +import net.corda.core.contracts.StateAndRef +import net.corda.core.flows.CollectSignaturesFlow +import net.corda.core.flows.FinalityFlow +import net.corda.core.flows.FlowException +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.ReceiveFinalityFlow +import net.corda.core.flows.SignTransactionFlow +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.ServiceHub +import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.serialize +import net.corda.core.transactions.LedgerTransaction 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.toHexString +import net.corda.core.utilities.unwrap import net.corda.finance.DOLLARS import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow @@ -22,10 +43,14 @@ import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStra import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetworkParameters import net.corda.testing.node.StartedMockNode +import net.corda.testing.node.TestCordapp import net.corda.testing.node.internal.FINANCE_CORDAPPS +import net.corda.testing.node.internal.enclosedCordapp import org.junit.After import org.junit.Before import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith class EncryptedTxEnclaveTest { @@ -37,7 +62,10 @@ class EncryptedTxEnclaveTest { private lateinit var bankOfCorda: Party private lateinit var aliceNode: StartedMockNode + private lateinit var alice: Party + private lateinit var bobNode: StartedMockNode + private lateinit var bob: Party private val serializerFactoriesForContexts = Caffeine.newBuilder() .maximumSize(128) @@ -46,16 +74,28 @@ class EncryptedTxEnclaveTest { private lateinit var serializationFactoryImpl: SerializationFactoryImpl - private val encryptedTxEnclave = EncryptedTxEnclave() + //private val encryptedTxEnclave = EncryptedTxEnclaveClient() @Before fun start() { - mockNet = MockNetwork(MockNetworkParameters(servicePeerAllocationStrategy = RoundRobin(), cordappsForAllNodes = FINANCE_CORDAPPS)) + mockNet = MockNetwork( + MockNetworkParameters( + servicePeerAllocationStrategy = RoundRobin(), + cordappsForAllNodes = listOf( + enclosedCordapp(), + TestCordapp.findCordapp("com.r3.conclave.encryptedtx.enclave" + ) + ) + ) + ) bankOfCordaNode = mockNet.createPartyNode(BOC_NAME) - bankOfCorda = bankOfCordaNode.info.identityFromX500Name(BOC_NAME) + bankOfCorda = bankOfCordaNode.info.singleIdentity() aliceNode = mockNet.createPartyNode(ALICE_NAME) + alice = aliceNode.info.singleIdentity() + bobNode = mockNet.createPartyNode(BOB_NAME) + bob = bobNode.info.singleIdentity() val serverScheme = AMQPServerSerializationScheme(emptyList(), serializerFactoriesForContexts) val clientScheme = AMQPServerSerializationScheme(emptyList(), serializerFactoriesForContexts) @@ -71,40 +111,246 @@ class EncryptedTxEnclaveTest { mockNet.stopNodes() } - @Test(timeout=300_000) - fun `pay some cash`() { - val payTo = aliceNode.info.singleIdentity() - val expectedPayment = 500.DOLLARS + @Test + fun `issue and move`() { - var future = bankOfCordaNode.startFlow(CashIssueFlow(initialBalance, ref, mockNet.defaultNotaryIdentity)) - val issuanceStx = future.getOrThrow().stx + val issuanceTx = bankOfCordaNode.execFlow(IssueFlow(300)) - val issuanceConclaveLedgerTxBytes = issuanceStx - .toLedgerTxModel(bankOfCordaNode.services) - .serialize(serializationFactoryImpl) - .bytes + val aliceTx = bankOfCordaNode.execFlow(MoveFlow(bankOfCordaNode.getAllTokens(), 200, alice)) - val encryptedTx = encryptedTxEnclave.encryptSignedTx(issuanceConclaveLedgerTxBytes) + val bobTx = aliceNode.execFlow(MoveFlow(aliceNode.getAllTokens(), 100, bob)) - future = bankOfCordaNode.startFlow(CashPaymentFlow(expectedPayment, payTo)) - mockNet.runNetwork() - val paymentStx = future.getOrThrow().stx - - val ledgerTxModel = paymentStx.toLedgerTxModel(bankOfCordaNode.services) - val txAndDependenciesBytes = VerifiableTxAndDependencies(ledgerTxModel, emptySet(), setOf(encryptedTx)).serialize() - - encryptedTxEnclave.verifyTx(txAndDependenciesBytes.bytes) - } - - private fun SignedTransaction.toLedgerTxModel(services: ServiceHub): ConclaveLedgerTxModel { - val ledgerTx = this.toLedgerTransaction(services) - - return ConclaveLedgerTxModel( - signedTransaction = this, - inputStates = ledgerTx.inputs.toTypedArray(), - attachments = ledgerTx.attachments.toTypedArray(), - networkParameters = ledgerTx.networkParameters!!, - references = ledgerTx.references.toTypedArray() + printTxs( listOf( + "Bank issue to self: " to issuanceTx, + "Bank pays Alice: " to aliceTx, + "Alice pays Bob: " to bobTx + ) ) } + + @Test + fun `issue and move with remote signer`() { + + println("Issue") + val issuanceTx = bankOfCordaNode.execFlow(IssueFlow(300)) + + println("Pay Alice") + val aliceTx = bankOfCordaNode.execFlow(MoveFlow(bankOfCordaNode.getAllTokens(), 200, alice, true)) + + println("Pay Bob") + val bobTx = aliceNode.execFlow(MoveFlow(aliceNode.getAllTokens(), 100, bob, true)) + + printTxs( listOf( + "Bank issue to self: " to issuanceTx, + "Bank pays Alice: " to aliceTx, + "Alice pays Bob: " to bobTx) + ) + } + + @Test + fun `bank of Corda cannot pay bob`() { + + bankOfCordaNode.execFlow(IssueFlow(300)) + + val exception = assertFailsWith{ + bankOfCordaNode.execFlow(MoveFlow(bankOfCordaNode.getAllTokens(), 200, bob, true)) + } + + assertEquals("java.lang.IllegalArgumentException: Bank of Corda cannot move money to Bob", exception.message) + } + + private fun printTxs(txHashes : List>) { + listOf(bankOfCordaNode, aliceNode, bobNode).forEach { node -> + println("------------------------") + println("${node.info.singleIdentity()}") + println("------------------------") + txHashes.forEach { labelToStx -> + val label = labelToStx.first + val stx = labelToStx.second + println("$label (${stx.id})") + println("> FOUND UNENCRYPTED: ${node.services.validatedTransactions.getTransaction(stx.id)}") + println("> FOUND ENCRYPTED: ${node.services.validatedTransactions.getEncryptedTransaction(stx.id)?.let { + "${shortStringDesc(it.encryptedBytes.toHexString())} signature ${it.sigs.map { sig -> sig.bytes.toHexString() }}" + }}") + + println() + } + println() + } + } + + private fun StartedMockNode.execFlow(flow : FlowLogic) : T { + val future = startFlow(flow) + mockNet.runNetwork() + return future.getOrThrow() + } + + private fun StartedMockNode.getAllTokens() : List> { + + val allStates = services.vaultService.queryBy(BasicToken::class.java).states + + println(this.info.singleIdentity()) + allStates.forEach { + println(it.state.data) + } + + return allStates.filter { + it.state.data.holder.owningKey == this.info.singleIdentity().owningKey + } + } + + private fun shortStringDesc(longString : String) : String { + return "EncryptedTransaction(${longString.take(15)}...${longString.takeLast(15)})" + } + + @CordaSerializable + enum class SignaturesRequired { + ALL, + SENDER_ONLY + } + + class BasicTokenContract: Contract { + + companion object { + val contractId = this::class.java.enclosingClass.canonicalName + } + + override fun verify(tx: LedgerTransaction) { + val command = tx.commandsOfType(BasicTokenCommand::class.java).single() + + when (command.value) { + is Issue -> { + val inputs = tx.inputsOfType() + val outputs = tx.outputsOfType() + + require(inputs.isEmpty()) { "No input states allowed" } + require(outputs.isNotEmpty()) { "At least one BasicToken input state is required" } + require(outputs.all { it.amount > 0 }) { "Outputs must have amounts greater than zero" } + + } + is Move -> { + val inputs = tx.inputsOfType() + val outputs = tx.outputsOfType() + + require(inputs.isNotEmpty() && outputs.isNotEmpty()) { "Input and output states are required" } + require(inputs.sumBy { it.amount } == outputs.sumBy { it.amount }) { "Inputs and outputs must have the same value"} + require(command.signers.containsAll(inputs.map { it.holder.owningKey })) { "All holders must sign the tx" } + + require(inputs.all { it.amount > 0 }) { "Inputs must have amounts greater than zero" } + require(outputs.all { it.amount > 0 }) { "Outputs must have amounts greater than zero" } + // no restriction on mixing issuers + } + } + } + open class BasicTokenCommand : CommandData + class Issue : BasicTokenCommand() + class Move : BasicTokenCommand() + } + + @BelongsToContract(BasicTokenContract::class) + class BasicToken( + val amount: Int, + val holder: AbstractParty, + override val participants : List = listOf(holder)) : ContractState { + + override fun equals(other: Any?): Boolean { + + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as BasicToken + if (amount != other.amount) return false + if (holder != other.holder) return false + return true + } + } + + class IssueFlow(val amount: Int): FlowLogic() { + @Suspendable + override fun call() : SignedTransaction { + val ourKey = ourIdentity.owningKey + + val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()) + .addCommand(BasicTokenContract.Issue(), ourKey) + .addOutputState(BasicToken(amount, ourIdentity)) + val stx = serviceHub.signInitialTransaction(tx, ourKey) + + return subFlow(FinalityFlow(stx, emptyList())) + } + } + + @InitiatingFlow + class MoveFlow(val inputs : List>, + val amount: Int, + val moveTo: AbstractParty, + val allMustSign : Boolean = false): FlowLogic() { + @Suspendable + override fun call() : SignedTransaction{ + val ourKey = ourIdentity.owningKey + + val allMustSignStatus = if(allMustSign) SignaturesRequired.ALL else SignaturesRequired.SENDER_ONLY + val signingKeys = if(allMustSign) listOf(ourKey, moveTo.owningKey) else listOf(ourKey) + + val changeAmount = inputs.sumBy { it.state.data.amount } - amount + + val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()) + .addCommand(BasicTokenContract.Move(), signingKeys) + .addOutputState(BasicToken(amount, moveTo)) + + if (changeAmount > 0) { + tx.addOutputState(BasicToken(changeAmount, ourIdentity)) + } + + inputs.forEach { + tx.addInputState(it) + } + + tx.verify(serviceHub) + + var stx = serviceHub.signInitialTransaction(tx, ourKey) + + val sessions = listOfNotNull( + serviceHub.identityService.wellKnownPartyFromAnonymous(moveTo) + ).filter { it.owningKey != ourKey }.map { initiateFlow(it) } + + sessions.forEach { + it.send(allMustSignStatus) + + if (allMustSign) { + stx = subFlow(CollectSignaturesFlow(stx , sessions)) + } + } + + return subFlow(FinalityFlow(stx, sessions)) + } + } + + @InitiatedBy(MoveFlow::class) + class MoveHandler(val otherSession: FlowSession): FlowLogic() { + @Suspendable + override fun call() { + if (!serviceHub.myInfo.isLegalIdentity(otherSession.counterparty)) { + + val requiresSignature = otherSession.receive(SignaturesRequired::class.java).unwrap { it } + + if (requiresSignature == SignaturesRequired.ALL) { + + subFlow(object : SignTransactionFlow(otherSession, encrypted = true) { + override fun checkTransaction(stx: SignedTransaction) { + val inputs = stx.tx.inputsStates.filterIsInstance>() + val outputs = stx.tx.outputsOfType(BasicToken::class.java) + + // a test condition we can use to trigger a signature failure + require(!(inputs.any { serviceHub.identityService.wellKnownPartyFromAnonymous(it.state.data.holder)?.name == BOC_NAME } && + outputs.any { serviceHub.identityService.wellKnownPartyFromAnonymous(it.holder)?.name == BOB_NAME })){ + "Bank of Corda cannot move money to Bob" + } + } + } + ) + } + + subFlow(ReceiveFinalityFlow(otherSideSession = otherSession)) + } + } + } } diff --git a/core/src/main/kotlin/net/corda/core/conclave/common/EnclaveClient.kt b/core/src/main/kotlin/net/corda/core/conclave/common/EnclaveClient.kt index a69033034c..6b79200758 100644 --- a/core/src/main/kotlin/net/corda/core/conclave/common/EnclaveClient.kt +++ b/core/src/main/kotlin/net/corda/core/conclave/common/EnclaveClient.kt @@ -1,19 +1,19 @@ package net.corda.core.conclave.common import net.corda.core.conclave.common.dto.ConclaveLedgerTxModel +import net.corda.core.conclave.common.dto.EncryptedVerifiableTxAndDependencies import net.corda.core.conclave.common.dto.VerifiableTxAndDependencies import net.corda.core.node.services.CordaService import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.EncryptedTransaction -@CordaService interface EnclaveClient { fun getEnclaveInstanceInfo() : ByteArray - fun enclaveVerifyAndEncrypt(txAndDependencies : VerifiableTxAndDependencies): EncryptedTransaction + fun enclaveVerifyAndEncrypt(txAndDependencies : VerifiableTxAndDependencies, checkSufficientSignatures: Boolean): EncryptedTransaction - fun enclaveVerify(encryptedTransaction : EncryptedTransaction): EncryptedTransaction + fun enclaveVerify(encryptedTxAndDependencies: EncryptedVerifiableTxAndDependencies): EncryptedTransaction fun encryptTransactionForLocal(encryptedTransaction: EncryptedTransaction): EncryptedTransaction @@ -31,11 +31,11 @@ class DummyEnclaveClient: EnclaveClient, SingletonSerializeAsToken() { throw UnsupportedOperationException("Add your custom enclave client implementation") } - override fun enclaveVerifyAndEncrypt(txAndDependencies: VerifiableTxAndDependencies): EncryptedTransaction { + override fun enclaveVerifyAndEncrypt(txAndDependencies: VerifiableTxAndDependencies, checkSufficientSignatures: Boolean): EncryptedTransaction { throw UnsupportedOperationException("Add your custom enclave client implementation") } - override fun enclaveVerify(encryptedTransaction: EncryptedTransaction): EncryptedTransaction { + override fun enclaveVerify(encryptedTxAndDependencies: EncryptedVerifiableTxAndDependencies) : EncryptedTransaction { throw UnsupportedOperationException("Add your custom enclave client implementation") } diff --git a/core/src/main/kotlin/net/corda/core/conclave/common/dto/EncryptedVerifiableTxAndDependencies.kt b/core/src/main/kotlin/net/corda/core/conclave/common/dto/EncryptedVerifiableTxAndDependencies.kt new file mode 100644 index 0000000000..843cdb2634 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/conclave/common/dto/EncryptedVerifiableTxAndDependencies.kt @@ -0,0 +1,12 @@ +package net.corda.core.conclave.common.dto + +import net.corda.core.serialization.CordaSerializable +import net.corda.core.transactions.EncryptedTransaction +import net.corda.core.transactions.SignedTransaction + +@CordaSerializable +data class EncryptedVerifiableTxAndDependencies( + val encryptedTransaction: EncryptedTransaction, + val dependencies: Set, + val encryptedDependencies: Set +) diff --git a/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt b/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt index 5b28415f16..cee92f1da5 100644 --- a/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/CollectSignaturesFlow.kt @@ -2,6 +2,8 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import com.sun.org.apache.xpath.internal.operations.Bool +import net.corda.core.conclave.common.dto.ConclaveLedgerTxModel +import net.corda.core.conclave.common.dto.VerifiableTxAndDependencies import net.corda.core.crypto.TransactionSignature import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.toStringShort @@ -276,7 +278,16 @@ abstract class SignTransactionFlow @JvmOverloads constructor(val otherSideSessio override fun call(): SignedTransaction { progressTracker.currentStep = RECEIVING // Receive transaction and resolve dependencies, check sufficient signatures is disabled as we don't have all signatures. - val stx = subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false, encrypted = true)) + + var conclaveLedgerTxModel : ConclaveLedgerTxModel? = null + + val stx = if (encrypted) { + conclaveLedgerTxModel = subFlow(ReceiveTransactionAsConclaveModelFlow(otherSideSession, checkSufficientSignatures = false, encrypted = true)) + conclaveLedgerTxModel.signedTransaction + } else { + subFlow(ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = false, encrypted = true)) + } + // Receive the signing key that the party requesting the signature expects us to sign with. Having this provided // means we only have to check we own that one key, rather than matching all keys in the transaction against all // keys we own.t @@ -294,44 +305,18 @@ abstract class SignTransactionFlow @JvmOverloads constructor(val otherSideSessio val validatedTxSvc = serviceHub.validatedTransactions val encryptedTxs = stx.dependencies.mapNotNull { - validatedTxId -> - validatedTxSvc.getEncryptedTransaction(validatedTxId)?.let { etx -> - etx.id to etx - } - }.toMap() + validatedTxSvc.getEncryptedTransaction(it) + }.toSet() val signedTxs = stx.dependencies.mapNotNull { - validatedTxId -> - validatedTxSvc.getTransaction(validatedTxId)?.let { stx -> - stx.id to stx - } - }.toMap() + validatedTxSvc.getTransaction(it) + }.toSet() -// val networkParameters = stx.dependencies.mapNotNull { depTxId -> -// val npHash = when { -// encryptedTxs[depTxId] != null -> serviceHub.encryptedTransactionService.getNetworkParameterHash(encryptedTxs[depTxId]!!) -// ?: serviceHub.networkParametersService.defaultHash -// signedTxs[depTxId] != null -> signedTxs[depTxId]!!.networkParametersHash -// ?: serviceHub.networkParametersService.defaultHash -// else -> null -// } -// -// npHash?.let { depTxId to npHash } -// }.associate { -// netParams -> -// netParams.first to serviceHub.networkParametersService.lookup(netParams.second) -// } - -// val rawDependencies = stx.dependencies.associate { -// txId -> -// txId to RawDependency( -// encryptedTxs[txId], -// signedTxs[txId], -// networkParameters[txId] -// ) -// } -// - // encryptionService.enclaveVerifyAndEncrypt() + encryptionService.enclaveVerifyAndEncrypt(VerifiableTxAndDependencies( + conclaveLedgerTxModel!!, + signedTxs, + encryptedTxs + ), false) } else { stx.tx.toLedgerTransaction(serviceHub).verify() diff --git a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt index 05dcc0cbf6..a2d0272e9f 100644 --- a/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/ReceiveTransactionFlow.kt @@ -31,17 +31,44 @@ import java.security.SignatureException * @property checkSufficientSignatures if true checks all required signatures are present. See [SignedTransaction.verify]. * @property statesToRecord which transaction states should be recorded in the vault, if any. */ + open class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSideSession: FlowSession, + private val checkSufficientSignatures: Boolean = true, + private val statesToRecord: StatesToRecord = StatesToRecord.NONE, + private val encrypted : Boolean = false) : + ReceiveTransactionFlowBase(otherSideSession, checkSufficientSignatures, statesToRecord, encrypted) { + + override fun getReturnVal(stx: SignedTransaction, conclaveLedgerTxModel: ConclaveLedgerTxModel?): SignedTransaction { + return stx + } +} + +open class ReceiveTransactionAsConclaveModelFlow @JvmOverloads constructor(private val otherSideSession: FlowSession, private val checkSufficientSignatures: Boolean = true, private val statesToRecord: StatesToRecord = StatesToRecord.NONE, - private val encrypted : Boolean = false) : FlowLogic() { + private val encrypted : Boolean = false) : + ReceiveTransactionFlowBase(otherSideSession, checkSufficientSignatures, statesToRecord, encrypted) { + + override fun getReturnVal(stx: SignedTransaction, conclaveLedgerTxModel: ConclaveLedgerTxModel?): ConclaveLedgerTxModel { + return conclaveLedgerTxModel ?: throw IllegalStateException("Cannot return a null ConclaveLedgerTxModel") + } +} + +abstract class ReceiveTransactionFlowBase @JvmOverloads constructor(private val otherSideSession: FlowSession, + private val checkSufficientSignatures: Boolean = true, + private val statesToRecord: StatesToRecord = StatesToRecord.NONE, + private val encrypted : Boolean = false) : FlowLogic() { + + @Suspendable + abstract fun getReturnVal(stx: SignedTransaction, conclaveLedgerTxModel: ConclaveLedgerTxModel?) : T + @Suppress("KDocMissingDocumentation") @Suspendable @Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class) - override fun call(): SignedTransaction { + override fun call(): T { if (checkSufficientSignatures) { logger.trace { "Receiving a transaction from ${otherSideSession.counterparty}" } } else { @@ -89,11 +116,16 @@ open class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSid }.toSet() val verifiableTx = VerifiableTxAndDependencies( - it.toLedgerTxModel(serviceHub), + conclaveLedgerTxModel!!, signedTxs, encryptedTxs ) - serviceHub.encryptedTransactionService.enclaveVerifyAndEncrypt(verifiableTx) + val encryptedAndVerifiedTx = serviceHub.encryptedTransactionService.enclaveVerifyAndEncrypt(verifiableTx, checkSufficientSignatures) + + if (checkSufficientSignatures) { + serviceHub.recordEncryptedTransactions(listOf(encryptedAndVerifiedTx)) + } + it } else { it.verify(serviceHub, checkSufficientSignatures) @@ -112,7 +144,7 @@ open class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSid serviceHub.recordTransactions(statesToRecord, setOf(stx)) logger.info("Successfully recorded received transaction locally.") } - return stx + return getReturnVal(stx, conclaveLedgerTxModel) } /** diff --git a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt index 7089794b9e..ef00291a44 100644 --- a/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/SendTransactionFlow.kt @@ -123,11 +123,11 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any, if (encrypted) { // The first step in an encrypted exchange, is to request an exchange of attestations - remoteAttestation = subFlow(ExchangeAttestationFlowHandler(otherSideSession)) + remoteAttestation = subFlow(ExchangeAttestationFlow(otherSideSession)) // also send the ledger transaction if (payload is SignedTransaction) { - val conclaveLedgerTxModel = payload.toLedgerTxModel(serviceHub) + val conclaveLedgerTxModel = payload.toLedgerTxModel(serviceHub, false) otherSideSession.send(conclaveLedgerTxModel) } } diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index d8a5b4c789..35c31fd6d1 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -10,9 +10,11 @@ import net.corda.core.crypto.SignableData import net.corda.core.crypto.SignatureMetadata import net.corda.core.crypto.TransactionSignature import net.corda.core.flows.ContractUpgradeFlow +import net.corda.core.internal.requireSupportedHashType import net.corda.core.node.services.* import net.corda.core.node.services.diagnostics.DiagnosticsService import net.corda.core.serialization.SerializeAsToken +import net.corda.core.transactions.EncryptedTransaction import net.corda.core.transactions.FilteredTransaction import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction @@ -236,6 +238,12 @@ interface ServiceHub : ServicesForResolution { recordTransactions(StatesToRecord.ONLY_RELEVANT, txs) } + /** + * Stores the given [EncryptedTransaction]s in the local transaction storage. + * This is expected to be run within a database transaction. + */ + fun recordEncryptedTransactions(txs: List) + /** * Converts the given [StateRef] into a [StateAndRef] object. * diff --git a/core/src/main/kotlin/net/corda/core/node/services/EncryptedTransactionService.kt b/core/src/main/kotlin/net/corda/core/node/services/EncryptedTransactionService.kt index 4253e9795b..087c810df6 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/EncryptedTransactionService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/EncryptedTransactionService.kt @@ -3,24 +3,23 @@ package net.corda.core.node.services import net.corda.core.conclave.common.DummyEnclaveClient import net.corda.core.conclave.common.EnclaveClient import net.corda.core.conclave.common.dto.ConclaveLedgerTxModel +import net.corda.core.conclave.common.dto.EncryptedVerifiableTxAndDependencies import net.corda.core.conclave.common.dto.VerifiableTxAndDependencies import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.EncryptedTransaction - -// TODO: this should be an interface class EncryptedTransactionService(val enclaveClient: EnclaveClient = DummyEnclaveClient()) : SingletonSerializeAsToken() { fun getEnclaveInstance() : ByteArray { return enclaveClient.getEnclaveInstanceInfo() } - fun enclaveVerifyAndEncrypt(txAndDependencies : VerifiableTxAndDependencies): EncryptedTransaction { - return enclaveClient.enclaveVerifyAndEncrypt(txAndDependencies) + fun enclaveVerifyAndEncrypt(txAndDependencies : VerifiableTxAndDependencies, checkSufficientSignatures: Boolean = true): EncryptedTransaction { + return enclaveClient.enclaveVerifyAndEncrypt(txAndDependencies, checkSufficientSignatures) } - fun enclaveVerifyAndEncrypt(encryptedTransaction: EncryptedTransaction) : EncryptedTransaction { - return enclaveClient.enclaveVerify(encryptedTransaction) + fun enclaveVerifyAndEncrypt(encryptedTxAndDependencies: EncryptedVerifiableTxAndDependencies) : EncryptedTransaction { + return enclaveClient.enclaveVerify(encryptedTxAndDependencies) } fun encryptTransactionForLocal(encryptedTransaction: EncryptedTransaction): EncryptedTransaction { diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index 750247dcbe..92966db9ff 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -378,8 +378,8 @@ constructor(val txBits: SerializedBytes, override val sigs: Lis // For Conclave PoC - fun toLedgerTxModel(services: ServiceHub): ConclaveLedgerTxModel { - val ledgerTx = toLedgerTransaction(services) + fun toLedgerTxModel(services: ServiceHub, checkSufficientSignatures: Boolean = true): ConclaveLedgerTxModel { + val ledgerTx = toLedgerTransaction(services, checkSufficientSignatures) return ConclaveLedgerTxModel( signedTransaction = this, diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 9d1adcfc81..ccf649f3fb 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -1055,8 +1055,21 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } private fun makeEncryptedTransactionService(): EncryptedTransactionService { - return cordappLoader.cordapps.map { it.services.filter { clazz -> clazz.interfaces.contains(EnclaveClient::class.java) }}.flatten().firstOrNull()?.let { - EncryptedTransactionService(it.newInstance() as EnclaveClient) + val clazz = cordappLoader.cordapps + .map { + it.cordappClasses + } + .flatten() + .firstOrNull { + try { + it.contains("EnclaveClient") && Class.forName(it).interfaces.contains(EnclaveClient::class.java) + } catch (e: NoClassDefFoundError) { + false + } + } + + return clazz?.let { + EncryptedTransactionService(Class.forName(it).getDeclaredConstructor().newInstance() as EnclaveClient) } ?: run { EncryptedTransactionService() } diff --git a/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt b/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt index 7380b5b012..4ad18ca144 100644 --- a/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt +++ b/node/src/main/kotlin/net/corda/node/services/DbTransactionsResolver.kt @@ -1,6 +1,7 @@ package net.corda.node.services import co.paralleluniverse.fibers.Suspendable +import net.corda.core.conclave.common.dto.EncryptedVerifiableTxAndDependencies import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic import net.corda.core.internal.FetchEncryptedTransactionsFlow @@ -189,7 +190,18 @@ class DbTransactionsResolver(private val flow: ResolveTransactionsFlow) : Transa "Somehow the unverified transaction ($txId) that we stored previously is no longer there." } if (!isVerified) { - val verifiedTransaction = encryptSvc.enclaveVerifyAndEncrypt(tx) + + // get the dependencies + val signedTransactions = tx.dependencies.mapNotNull { transactionStorage.getTransaction(it) }.toSet() + val encryptedTransactions = tx.dependencies.mapNotNull { transactionStorage.getEncryptedTransaction(it) }.toSet() + + val verifiedTransaction = encryptSvc.enclaveVerifyAndEncrypt( + EncryptedVerifiableTxAndDependencies( + tx, + signedTransactions, + encryptedTransactions + ) + ) // TODO: why does this usually go through the serviceHub's recordTransactions function and not // direct to the validatedTransactions service?? diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index 3ce83ce7bc..2422ecd91e 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -64,6 +64,19 @@ interface ServiceHubInternal : ServiceHubCoreInternal { return sort.complete() } + fun recordEncryptedTransactions(txs: List, + validatedTransactions: WritableTransactionStorage, + database: CordaPersistence) { + + database.transaction { + require(txs.isNotEmpty()) { "No encrypted transactions passed in for recording" } + + txs.forEach { + validatedTransactions.addVerifiedEncryptedTransaction(it) + } + } + } + fun recordTransactions(statesToRecord: StatesToRecord, txs: Collection, validatedTransactions: WritableTransactionStorage, @@ -168,6 +181,15 @@ interface ServiceHubInternal : ServiceHubCoreInternal { ) } + override fun recordEncryptedTransactions(txs: List) { + txs.forEach { requireSupportedHashType(it) } + Companion.recordEncryptedTransactions( + txs, + validatedTransactions, + database + ) + } + override fun createTransactionsResolver(flow: ResolveTransactionsFlow): TransactionsResolver = DbTransactionsResolver(flow) /** diff --git a/node/src/test/kotlin/net/corda/node/services/encryptedtx/EncryptedBackchainTests.kt b/node/src/test/kotlin/net/corda/node/services/encryptedtx/EncryptedBackchainTests.kt deleted file mode 100644 index e89db7d695..0000000000 --- a/node/src/test/kotlin/net/corda/node/services/encryptedtx/EncryptedBackchainTests.kt +++ /dev/null @@ -1,304 +0,0 @@ -package net.corda.node.services.encryptedtx - -import co.paralleluniverse.fibers.Suspendable -import net.corda.core.contracts.BelongsToContract -import net.corda.core.contracts.CommandData -import net.corda.core.contracts.Contract -import net.corda.core.contracts.ContractState -import net.corda.core.contracts.StateAndRef -import net.corda.core.flows.CollectSignaturesFlow -import net.corda.core.flows.FinalityFlow -import net.corda.core.flows.FlowException -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowSession -import net.corda.core.flows.InitiatedBy -import net.corda.core.flows.InitiatingFlow -import net.corda.core.flows.ReceiveFinalityFlow -import net.corda.core.flows.SignTransactionFlow -import net.corda.core.identity.AbstractParty -import net.corda.core.identity.Party -import net.corda.core.serialization.CordaSerializable -import net.corda.core.transactions.LedgerTransaction -import net.corda.core.transactions.SignedTransaction -import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.getOrThrow -import net.corda.core.utilities.toHexString -import net.corda.core.utilities.unwrap -import net.corda.node.services.persistence.DBCheckpointStorage -import net.corda.testing.core.ALICE_NAME -import net.corda.testing.core.BOB_NAME -import net.corda.testing.core.BOC_NAME -import net.corda.testing.core.singleIdentity -import net.corda.testing.node.InMemoryMessagingNetwork -import net.corda.testing.node.MockNetwork -import net.corda.testing.node.MockNetworkParameters -import net.corda.testing.node.StartedMockNode -import net.corda.testing.node.internal.enclosedCordapp -import org.junit.Before -import org.junit.Test -import java.lang.IllegalArgumentException -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith - -class EncryptedBackchainTests { - - private lateinit var mockNet: MockNetwork - private lateinit var bankOfCordaNode: StartedMockNode - private lateinit var bankOfCorda: Party - private lateinit var aliceNode: StartedMockNode - private lateinit var alice: Party - private lateinit var bobNode: StartedMockNode - private lateinit var bob: Party - - @Before - fun start() { - mockNet = MockNetwork(MockNetworkParameters(servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin(), cordappsForAllNodes = listOf(enclosedCordapp()))) - bankOfCordaNode = mockNet.createPartyNode(BOC_NAME) - bankOfCorda = bankOfCordaNode.info.identityFromX500Name(BOC_NAME) - aliceNode = mockNet.createPartyNode(ALICE_NAME) - alice = aliceNode.info.singleIdentity() - bobNode = mockNet.createPartyNode(BOB_NAME) - bob = bobNode.info.singleIdentity() - } - - @Test - fun `issue and move`() { - - val issuanceTx = bankOfCordaNode.execFlow(IssueFlow(300)) - - val aliceTx = bankOfCordaNode.execFlow(MoveFlow(bankOfCordaNode.getAllTokens(), 200, alice)) - - val bobTx = aliceNode.execFlow(MoveFlow(aliceNode.getAllTokens(), 100, bob)) - - printTxs( listOf( - "Bank issue to self: " to issuanceTx, - "Bank pays Alice: " to aliceTx, - "Alice pays Bob: " to bobTx) - ) - } - - @Test - fun `issue and move with remote signer`() { - - val issuanceTx = bankOfCordaNode.execFlow(IssueFlow(300)) - - val aliceTx = bankOfCordaNode.execFlow(MoveFlow(bankOfCordaNode.getAllTokens(), 200, alice, true)) - - val bobTx = aliceNode.execFlow(MoveFlow(aliceNode.getAllTokens(), 100, bob, true)) - - printTxs( listOf( - "Bank issue to self: " to issuanceTx, - "Bank pays Alice: " to aliceTx, - "Alice pays Bob: " to bobTx) - ) - } - - @Test - fun `bank of Corda cannot pay bob`() { - - bankOfCordaNode.execFlow(IssueFlow(300)) - - val exception = assertFailsWith{ - bankOfCordaNode.execFlow(MoveFlow(bankOfCordaNode.getAllTokens(), 200, bob, true)) - } - - assertEquals("java.lang.IllegalArgumentException: Bank of Corda cannot move money to Bob", exception.message) - } - - private fun printTxs(txHashes : List>) { - listOf(bankOfCordaNode, aliceNode, bobNode).forEach { node -> - println("------------------------") - println("${node.info.singleIdentity()}") - println("------------------------") - txHashes.forEach { labelToStx -> - val label = labelToStx.first - val stx = labelToStx.second - println("$label (${stx.id})") - println("> FOUND UNENCRYPTED: ${node.services.validatedTransactions.getTransaction(stx.id)}") - println("> FOUND ENCRYPTED: ${node.services.validatedTransactions.getEncryptedTransaction(stx.id)?.let { - "${shortStringDesc(it.encryptedBytes.toHexString())} signature ${it.sigs.map { sig -> sig.bytes.toHexString() }}" - }}") - - println() - } - println() - } - } - - private fun StartedMockNode.execFlow(flow : FlowLogic) : T { - val future = startFlow(flow) - mockNet.runNetwork() - return future.getOrThrow() - } - - private fun StartedMockNode.getAllTokens() : List> { - - val allStates = services.vaultService.queryBy(BasicToken::class.java).states - - println(this.info.singleIdentity()) - allStates.forEach { - println(it.state.data) - } - - return allStates.filter { - it.state.data.holder.owningKey == this.info.singleIdentity().owningKey - } - } - - private fun shortStringDesc(longString : String) : String { - return "EncryptedTransaction(${longString.take(15)}...${longString.takeLast(15)})" - } - - @CordaSerializable - enum class SignaturesRequired { - ALL, - SENDER_ONLY - } - - class BasicTokenContract: Contract { - - companion object { - val contractId = this::class.java.enclosingClass.canonicalName - } - - override fun verify(tx: LedgerTransaction) { - val command = tx.commandsOfType(BasicTokenCommand::class.java).single() - - when (command.value) { - is Issue -> { - val inputs = tx.inputsOfType() - val outputs = tx.outputsOfType() - - require(inputs.isEmpty()) { "No input states allowed" } - require(outputs.isNotEmpty()) { "At least one BasicToken input state is required" } - require(outputs.all { it.amount > 0 }) { "Outputs must have amounts greater than zero" } - - } - is Move -> { - val inputs = tx.inputsOfType() - val outputs = tx.outputsOfType() - - require(inputs.isNotEmpty() && outputs.isNotEmpty()) { "Input and output states are required" } - require(inputs.sumBy { it.amount } == outputs.sumBy { it.amount }) { "Inputs and outputs must have the same value"} - require(command.signers.containsAll(inputs.map { it.holder.owningKey })) { "All holders must sign the tx" } - - require(inputs.all { it.amount > 0 }) { "Inputs must have amounts greater than zero" } - require(outputs.all { it.amount > 0 }) { "Outputs must have amounts greater than zero" } - // no restriction on mixing issuers - } - } - } - open class BasicTokenCommand : CommandData - class Issue : BasicTokenCommand() - class Move : BasicTokenCommand() - } - - @BelongsToContract(BasicTokenContract::class) - class BasicToken( - val amount: Int, - val holder: AbstractParty, - override val participants : List = listOf(holder)) : ContractState { - - override fun equals(other: Any?): Boolean { - - if (this === other) return true - if (javaClass != other?.javaClass) return false - other as BasicToken - if (amount != other.amount) return false - if (holder != other.holder) return false - return true - } - } - - class IssueFlow(val amount: Int): FlowLogic() { - @Suspendable - override fun call() : SignedTransaction { - val ourKey = ourIdentity.owningKey - - val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()) - .addCommand(BasicTokenContract.Issue(), ourKey) - .addOutputState(BasicToken(amount, ourIdentity)) - val stx = serviceHub.signInitialTransaction(tx, ourKey) - - return subFlow(FinalityFlow(stx, emptyList())) - } - } - - @InitiatingFlow - class MoveFlow(val inputs : List>, - val amount: Int, - val moveTo: AbstractParty, - val allMustSign : Boolean = false): FlowLogic() { - @Suspendable - override fun call() : SignedTransaction{ - val ourKey = ourIdentity.owningKey - - val allMustSignStatus = if(allMustSign) SignaturesRequired.ALL else SignaturesRequired.SENDER_ONLY - val signingKeys = if(allMustSign) listOf(ourKey, moveTo.owningKey) else listOf(ourKey) - - val changeAmount = inputs.sumBy { it.state.data.amount } - amount - - val tx = TransactionBuilder(serviceHub.networkMapCache.notaryIdentities.first()) - .addCommand(BasicTokenContract.Move(), signingKeys) - .addOutputState(BasicToken(amount, moveTo)) - - if (changeAmount > 0) { - tx.addOutputState(BasicToken(changeAmount, ourIdentity)) - } - - inputs.forEach { - tx.addInputState(it) - } - - tx.verify(serviceHub) - - var stx = serviceHub.signInitialTransaction(tx, ourKey) - - val wtx = stx.tx - - val sessions = listOfNotNull( - serviceHub.identityService.wellKnownPartyFromAnonymous(moveTo) - ).filter { it.owningKey != ourKey }.map { initiateFlow(it) } - - sessions.forEach { - it.send(allMustSignStatus) - - if (allMustSign) { - stx = subFlow(CollectSignaturesFlow(stx , sessions)) - } - } - - return subFlow(FinalityFlow(stx, sessions)) - } - } - - @InitiatedBy(MoveFlow::class) - class MoveHandler(val otherSession: FlowSession): FlowLogic() { - @Suspendable - override fun call() { - if (!serviceHub.myInfo.isLegalIdentity(otherSession.counterparty)) { - - val requiresSignature = otherSession.receive(SignaturesRequired::class.java).unwrap { it } - - if (requiresSignature == SignaturesRequired.ALL) { - - subFlow(object : SignTransactionFlow(otherSession, encrypted = true) { - override fun checkTransaction(stx: SignedTransaction) { - val inputs = stx.tx.inputsStates.filterIsInstance>() - val outputs = stx.tx.outputsOfType(BasicToken::class.java) - - // a test condition we can use to trigger a signature failure - require(!(inputs.any { serviceHub.identityService.wellKnownPartyFromAnonymous(it.state.data.holder)?.name == BOC_NAME } && - outputs.any { serviceHub.identityService.wellKnownPartyFromAnonymous(it.holder)?.name == BOB_NAME })){ - "Bank of Corda cannot move money to Bob" - } - } - } - ) - } - - subFlow(ReceiveFinalityFlow(otherSideSession = otherSession)) - } - } - } -} \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt index 74d3fe20e2..a2a2f86b93 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -22,6 +22,7 @@ import net.corda.core.node.services.* import net.corda.core.node.services.diagnostics.DiagnosticsService import net.corda.core.node.services.vault.CordaTransactionSupport import net.corda.core.serialization.SerializeAsToken +import net.corda.core.transactions.EncryptedTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.NetworkHostAndPort import net.corda.node.VersionInfo @@ -432,6 +433,12 @@ open class MockServices private constructor( } } + override fun recordEncryptedTransactions(txs: List) { + txs.forEach { + (validatedTransactions as WritableTransactionStorage).addVerifiedEncryptedTransaction(it) + } + } + override val networkParameters: NetworkParameters get() = networkParametersService.run { lookup(currentHash)!! }