mirror of
https://github.com/corda/corda.git
synced 2025-02-02 01:08:09 +00:00
Enclave tests working
This commit is contained in:
parent
3ed48a99f7
commit
95b80b37b7
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
)
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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??
|
||||
|
@ -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)
|
||||
|
||||
/**
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)!! }
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user