Enclave tests working

This commit is contained in:
adam.houston 2022-03-15 11:53:38 +00:00
parent 3ed48a99f7
commit 95b80b37b7
15 changed files with 534 additions and 429 deletions

View File

@ -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<EncryptedTransaction>): Set<SignedTransaction> {
// 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()
}
}
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)
}
}

View File

@ -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<FlowException>{
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<Pair<String,SignedTransaction>>) {
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 <T> StartedMockNode.execFlow(flow : FlowLogic<T>) : T {
val future = startFlow(flow)
mockNet.runNetwork()
return future.getOrThrow()
}
private fun StartedMockNode.getAllTokens() : List<StateAndRef<BasicToken>> {
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<BasicToken>()
val outputs = tx.outputsOfType<BasicToken>()
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<BasicToken>()
val outputs = tx.outputsOfType<BasicToken>()
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<AbstractParty> = 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<SignedTransaction>() {
@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<StateAndRef<BasicToken>>,
val amount: Int,
val moveTo: AbstractParty,
val allMustSign : Boolean = false): FlowLogic<SignedTransaction>() {
@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<Unit>() {
@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<StateAndRef<BasicToken>>()
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))
}
}
}
}

View File

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

View File

@ -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<SignedTransaction>,
val encryptedDependencies: Set<EncryptedTransaction>
)

View File

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

View File

@ -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<SignedTransaction>(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<SignedTransaction>() {
private val encrypted : Boolean = false) :
ReceiveTransactionFlowBase<ConclaveLedgerTxModel>(otherSideSession, checkSufficientSignatures, statesToRecord, encrypted) {
override fun getReturnVal(stx: SignedTransaction, conclaveLedgerTxModel: ConclaveLedgerTxModel?): ConclaveLedgerTxModel {
return conclaveLedgerTxModel ?: throw IllegalStateException("Cannot return a null ConclaveLedgerTxModel")
}
}
abstract class ReceiveTransactionFlowBase<T> @JvmOverloads constructor(private val otherSideSession: FlowSession,
private val checkSufficientSignatures: Boolean = true,
private val statesToRecord: StatesToRecord = StatesToRecord.NONE,
private val encrypted : Boolean = false) : FlowLogic<T>() {
@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)
}
/**

View File

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

View File

@ -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<EncryptedTransaction>)
/**
* Converts the given [StateRef] into a [StateAndRef] object.
*

View File

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

View File

@ -378,8 +378,8 @@ constructor(val txBits: SerializedBytes<CoreTransaction>, 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,

View File

@ -1055,8 +1055,21 @@ abstract class AbstractNode<S>(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()
}

View File

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

View File

@ -64,6 +64,19 @@ interface ServiceHubInternal : ServiceHubCoreInternal {
return sort.complete()
}
fun recordEncryptedTransactions(txs: List<EncryptedTransaction>,
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<SignedTransaction>,
validatedTransactions: WritableTransactionStorage,
@ -168,6 +181,15 @@ interface ServiceHubInternal : ServiceHubCoreInternal {
)
}
override fun recordEncryptedTransactions(txs: List<EncryptedTransaction>) {
txs.forEach { requireSupportedHashType(it) }
Companion.recordEncryptedTransactions(
txs,
validatedTransactions,
database
)
}
override fun createTransactionsResolver(flow: ResolveTransactionsFlow): TransactionsResolver = DbTransactionsResolver(flow)
/**

View File

@ -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<FlowException>{
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<Pair<String,SignedTransaction>>) {
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 <T> StartedMockNode.execFlow(flow : FlowLogic<T>) : T {
val future = startFlow(flow)
mockNet.runNetwork()
return future.getOrThrow()
}
private fun StartedMockNode.getAllTokens() : List<StateAndRef<BasicToken>> {
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<BasicToken>()
val outputs = tx.outputsOfType<BasicToken>()
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<BasicToken>()
val outputs = tx.outputsOfType<BasicToken>()
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<AbstractParty> = 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<SignedTransaction>() {
@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<StateAndRef<BasicToken>>,
val amount: Int,
val moveTo: AbstractParty,
val allMustSign : Boolean = false): FlowLogic<SignedTransaction>() {
@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<Unit>() {
@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<StateAndRef<BasicToken>>()
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))
}
}
}
}

View File

@ -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<EncryptedTransaction>) {
txs.forEach {
(validatedTransactions as WritableTransactionStorage).addVerifiedEncryptedTransaction(it)
}
}
override val networkParameters: NetworkParameters
get() = networkParametersService.run { lookup(currentHash)!! }