Example of encrypted backchain - WIP for investigation purposes

This commit is contained in:
adam.houston 2022-02-18 16:24:37 +00:00
parent 0a21fda87c
commit a165c69d3a
27 changed files with 900 additions and 45 deletions

View File

@ -21,7 +21,7 @@ jdkClassifier11=jdk11
dockerJavaVersion=3.2.5
proguardVersion=6.1.1
bouncycastleVersion=1.68
classgraphVersion=4.8.90
classgraphVersion=4.8.135
disruptorVersion=3.4.2
typesafeConfigVersion=1.3.4
jsr305Version=3.0.2

View File

@ -180,7 +180,8 @@ class FinalityFlow private constructor(val transaction: SignedTransaction,
oldV3Broadcast(notarised, oldParticipants.toSet())
for (session in sessions) {
try {
subFlow(SendTransactionFlow(session, notarised))
// PoC send encrypted
subFlow(SendTransactionFlow(session, notarised, true))
logger.info("Party ${session.counterparty} received the transaction.")
} catch (e: UnexpectedFlowEndException) {
throw UnexpectedFlowEndException(
@ -282,7 +283,7 @@ class ReceiveFinalityFlow @JvmOverloads constructor(private val otherSideSession
private val statesToRecord: StatesToRecord = ONLY_RELEVANT) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
return subFlow(object : ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = true, statesToRecord = statesToRecord) {
return subFlow(object : ReceiveTransactionFlow(otherSideSession, checkSufficientSignatures = true, statesToRecord = statesToRecord, encrypted = true) {
override fun checkBeforeRecording(stx: SignedTransaction) {
require(expectedTxId == null || expectedTxId == stx.id) {
"We expected to receive transaction with ID $expectedTxId but instead got ${stx.id}. Transaction was" +

View File

@ -4,8 +4,10 @@ import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.*
import net.corda.core.internal.ResolveTransactionsFlow
import net.corda.core.internal.checkParameterHash
import net.corda.core.internal.dependencies
import net.corda.core.internal.pushToLoggingContext
import net.corda.core.node.StatesToRecord
import net.corda.core.transactions.RawDependency
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.trace
import net.corda.core.utilities.unwrap
@ -30,7 +32,8 @@ import java.security.SignatureException
*/
open class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSideSession: FlowSession,
private val checkSufficientSignatures: Boolean = true,
private val statesToRecord: StatesToRecord = StatesToRecord.NONE) : FlowLogic<SignedTransaction>() {
private val statesToRecord: StatesToRecord = StatesToRecord.NONE,
private val encrypted : Boolean = false) : FlowLogic<SignedTransaction>() {
@Suppress("KDocMissingDocumentation")
@Suspendable
@Throws(SignatureException::class,
@ -47,11 +50,59 @@ open class ReceiveTransactionFlow @JvmOverloads constructor(private val otherSid
it.pushToLoggingContext()
logger.info("Received transaction acknowledgement request from party ${otherSideSession.counterparty}.")
checkParameterHash(it.networkParametersHash)
subFlow(ResolveTransactionsFlow(it, otherSideSession, statesToRecord))
subFlow(ResolveTransactionsFlow(it, otherSideSession, statesToRecord, encrypted = encrypted))
logger.info("Transaction dependencies resolution completed.")
try {
if (encrypted) {
val validatedTxSvc = serviceHub.validatedTransactions
val encryptedTxs = it.dependencies.mapNotNull {
validatedTxId ->
validatedTxSvc.getEncryptedTransaction(validatedTxId)?.let { etx ->
etx.id to etx
}
}.toMap()
val signedTxs = it.dependencies.mapNotNull {
validatedTxId ->
validatedTxSvc.getTransaction(validatedTxId)?.let { stx ->
stx.id to stx
}
}.toMap()
val networkParameters = it.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 = it.dependencies.associate {
txId ->
txId to RawDependency(
encryptedTxs[txId],
signedTxs[txId],
networkParameters[txId]
)
}
serviceHub.encryptedTransactionService.verifyTransaction(it, serviceHub, checkSufficientSignatures, rawDependencies)
it
} else {
it.verify(serviceHub, checkSufficientSignatures)
it
}
} catch (e: Exception) {
logger.warn("Transaction verification failed.")
throw e

View File

@ -67,7 +67,7 @@ class MaybeSerializedSignedTransaction(override val id: SecureHash, val serializ
* @param otherSide the target party.
* @param stx the [SignedTransaction] being sent to the [otherSideSession].
*/
open class SendTransactionFlow(otherSide: FlowSession, stx: SignedTransaction) : DataVendingFlow(otherSide, stx)
open class SendTransactionFlow(otherSide: FlowSession, stx: SignedTransaction, override val encrypted: Boolean = false) : DataVendingFlow(otherSide, stx, encrypted)
/**
* The [SendStateAndRefFlow] should be used to send a list of input [StateAndRef] to another peer that wishes to verify
@ -80,7 +80,7 @@ open class SendTransactionFlow(otherSide: FlowSession, stx: SignedTransaction) :
*/
open class SendStateAndRefFlow(otherSideSession: FlowSession, stateAndRefs: List<StateAndRef<*>>) : DataVendingFlow(otherSideSession, stateAndRefs)
open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any) : FlowLogic<Void?>() {
open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any, open val encrypted: Boolean = false) : FlowLogic<Void?>() {
@Suspendable
protected open fun sendPayloadAndReceiveDataRequest(otherSideSession: FlowSession, payload: Any) = otherSideSession.sendAndReceive<FetchDataFlow.Request>(payload)
@ -91,6 +91,9 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any)
@Suspendable
override fun call(): Void? {
val encryptSvc = serviceHub.encryptedTransactionService
val networkMaxMessageSize = serviceHub.networkParameters.maxMessageSize
val maxPayloadSize = networkMaxMessageSize / 2
@ -146,10 +149,31 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any)
var numSent = 0
payload = when (dataRequest.dataType) {
FetchDataFlow.DataType.TRANSACTION -> dataRequest.hashes.map { txId ->
logger.trace { "Sending: TRANSACTION (dataRequest.hashes.size=${dataRequest.hashes.size})" }
logger.trace { "Sending: TRANSACTION (dataRequest.hashes.size=${dataRequest.hashes.size}), encrypted: $encrypted" }
if (!authorisedTransactions.isAuthorised(txId)) {
throw FetchDataFlow.IllegalTransactionRequest(txId)
}
if (encrypted) {
var encryptedTx = serviceHub.validatedTransactions.getEncryptedTransaction(txId)
if (encryptedTx == null) {
val tx = serviceHub.validatedTransactions.getTransaction(txId)
?: throw FetchDataFlow.HashNotFound(txId)
encryptedTx = encryptSvc.encryptTransaction(tx)
}
authorisedTransactions.removeAuthorised(encryptedTx.id)
authorisedTransactions.addAuthorised(encryptSvc.getDependencies(encryptedTx))
totalByteCount += encryptedTx.bytes.size
numSent++
encryptedTx
} else {
val tx = serviceHub.validatedTransactions.getTransaction(txId)
?: throw FetchDataFlow.HashNotFound(txId)
authorisedTransactions.removeAuthorised(tx.id)
@ -158,8 +182,15 @@ open class DataVendingFlow(val otherSideSession: FlowSession, val payload: Any)
numSent++
tx
}
}
// Loop on all items returned using dataRequest.hashes.map:
FetchDataFlow.DataType.BATCH_TRANSACTION -> dataRequest.hashes.map { txId ->
// TODO: adding this for PoC to ensure that we don't fall into this
if (encrypted) {
throw FlowException("Batch mode disabled for encryption PoC")
}
if (!authorisedTransactions.isAuthorised(txId)) {
throw FetchDataFlow.IllegalTransactionRequest(txId)
}

View File

@ -18,6 +18,7 @@ import net.corda.core.serialization.CordaSerializationTransformEnumDefaults
import net.corda.core.serialization.SerializationToken
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.serialization.SerializeAsTokenContext
import net.corda.core.transactions.EncryptedTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.UntrustworthyData
@ -273,6 +274,12 @@ class FetchTransactionsFlow(requests: Set<SecureHash>, otherSide: FlowSession) :
override fun load(txid: SecureHash): SignedTransaction? = serviceHub.validatedTransactions.getTransaction(txid)
}
class FetchEncryptedTransactionsFlow(requests: Set<SecureHash>, otherSide: FlowSession) :
FetchDataFlow<EncryptedTransaction, EncryptedTransaction>(requests, otherSide, DataType.TRANSACTION) {
override fun load(txid: SecureHash): EncryptedTransaction? = serviceHub.validatedTransactions.getEncryptedTransaction(txid)
}
class FetchBatchTransactionsFlow(requests: Set<SecureHash>, otherSide: FlowSession) :
FetchDataFlow<MaybeSerializedSignedTransaction, MaybeSerializedSignedTransaction>(requests, otherSide, DataType.BATCH_TRANSACTION) {

View File

@ -7,6 +7,7 @@ import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.node.StatesToRecord
import net.corda.core.transactions.ContractUpgradeWireTransaction
import net.corda.core.transactions.EncryptedTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.debug
@ -21,7 +22,8 @@ class ResolveTransactionsFlow private constructor(
val initialTx: SignedTransaction?,
val txHashes: Set<SecureHash>,
val otherSide: FlowSession,
val statesToRecord: StatesToRecord
val statesToRecord: StatesToRecord,
val encrypted: Boolean = false
) : FlowLogic<Unit>() {
constructor(txHashes: Set<SecureHash>, otherSide: FlowSession, statesToRecord: StatesToRecord = StatesToRecord.NONE)
@ -36,6 +38,10 @@ class ResolveTransactionsFlow private constructor(
constructor(transaction: SignedTransaction, otherSide: FlowSession, statesToRecord: StatesToRecord = StatesToRecord.NONE)
: this(transaction, transaction.dependencies, otherSide, statesToRecord)
// TODO: PoC constructor
constructor(transaction: SignedTransaction, otherSide: FlowSession, statesToRecord: StatesToRecord = StatesToRecord.NONE, encrypted: Boolean)
: this(transaction, transaction.dependencies, otherSide, statesToRecord, encrypted)
private var fetchNetParamsFromCounterpart = false
@Suppress("MagicNumber")
@ -49,16 +55,25 @@ class ResolveTransactionsFlow private constructor(
// Fetch missing parameters flow was added in version 4. This check is needed so we don't end up with node V4 sending parameters
// request to node V3 that doesn't know about this protocol.
fetchNetParamsFromCounterpart = counterpartyPlatformVersion >= PlatformVersionSwitches.FETCH_MISSING_NETWORK_PARAMETERS
val batchMode = counterpartyPlatformVersion >= PlatformVersionSwitches.BATCH_DOWNLOAD_COUNTERPARTY_BACKCHAIN
// disable batch mode for encrypted
val batchMode = counterpartyPlatformVersion >= PlatformVersionSwitches.BATCH_DOWNLOAD_COUNTERPARTY_BACKCHAIN && !encrypted
logger.debug { "ResolveTransactionsFlow.call(): Otherside Platform Version = '$counterpartyPlatformVersion': Batch mode = $batchMode" }
// TODO: attachments and net params are not encrypted (yet)
if (initialTx != null) {
fetchMissingAttachments(initialTx)
fetchMissingNetworkParameters(initialTx)
}
val resolver = (serviceHub as ServiceHubCoreInternal).createTransactionsResolver(this)
if (encrypted) {
resolver.downloadEncryptedDependencies()
} else {
resolver.downloadDependencies(batchMode)
}
logger.trace { "ResolveTransactionsFlow: Sending END." }
otherSide.send(FetchDataFlow.Request.End) // Finish fetching data.
@ -66,8 +81,13 @@ class ResolveTransactionsFlow private constructor(
// If transaction resolution is performed for a transaction where some states are relevant, then those should be
// recorded if this has not already occurred.
val usedStatesToRecord = if (statesToRecord == StatesToRecord.NONE) StatesToRecord.ONLY_RELEVANT else statesToRecord
if (encrypted) {
resolver.recordEncryptedDependencies(usedStatesToRecord)
} else {
resolver.recordDependencies(usedStatesToRecord)
}
}
/**
* Fetches the set of attachments required to verify the given transaction. If these are not already present, they will be fetched from
@ -108,4 +128,25 @@ class ResolveTransactionsFlow private constructor(
false
}
}
// PoC variants.
// TODO: no support for contract upgrade!
@Suspendable
fun fetchMissingAttachments(transaction: EncryptedTransaction): Boolean {
val attachmentIds = serviceHub.encryptedTransactionService.getAttachmentIds(transaction)
val downloads = subFlow(FetchAttachmentsFlow(attachmentIds, otherSide)).downloaded
return (downloads.isNotEmpty())
}
@Suspendable
fun fetchMissingNetworkParameters(transaction: EncryptedTransaction): Boolean {
return if (fetchNetParamsFromCounterpart) {
serviceHub.encryptedTransactionService.getNetworkParameterHash(transaction)?.let {
val downloads = subFlow(FetchNetworkParametersFlow(setOf(it), otherSide)).downloaded
downloads.isNotEmpty()
} ?: false
} else {
false
}
}
}

View File

@ -32,4 +32,12 @@ interface TransactionsResolver {
@Suspendable
fun recordDependencies(usedStatesToRecord: StatesToRecord)
// for Poc we will create a completely parallel set of functions, perhaps a different implementation of TransactionsResolver is
// preferable long term
@Suspendable
fun downloadEncryptedDependencies()
@Suspendable
fun recordEncryptedDependencies(usedStatesToRecord: StatesToRecord)
}

View File

@ -438,3 +438,7 @@ class ContractVerifier(private val transactionClassLoader: ClassLoader) : Functi
}
}
}
// BOB
// E32DC1E8E08D41FE635B27DED128193B75833DC2053BD6A8BB52FB21448EF045 -> 68BB0EA190E6CCD4A5E39CCCEE18A27
// E32DC1E8E08D41FE635B27DED128193B75833DC2053BD6A8BB52FB21448EF045 -> 68BB0EA190E6CCD4A5E39CCCEE18A27

View File

@ -49,6 +49,8 @@ interface ServicesForResolution {
/** Returns the network parameters the node is operating under. */
val networkParameters: NetworkParameters
val encryptedTransactionService: EncryptedTransactionService
/**
* Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState].
*
@ -120,6 +122,8 @@ interface ServiceHub : ServicesForResolution {
// NOTE: Any services exposed to flows (public view) need to implement [SerializeAsToken] or similar to avoid
// their internal state from being serialized in checkpoints.
override val encryptedTransactionService: EncryptedTransactionService
/**
* The vault service lets you observe, soft lock and add notes to states that involve you or are relevant to your
* node in some way. Additionally you may query and track states that correspond to various criteria.

View File

@ -0,0 +1,149 @@
package net.corda.core.node.services
import net.corda.core.contracts.StateRef
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.internal.dependencies
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.transactions.EncryptedTransaction
import net.corda.core.transactions.RawDependency
import net.corda.core.transactions.RawDependencyMap
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.SignedTransactionDependencies
import net.corda.core.transactions.SignedTransactionDependencyMap
import net.corda.core.utilities.toHexString
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.spec.IvParameterSpec
// TODO: this should be an interface
class EncryptedTransactionService() : SingletonSerializeAsToken() {
companion object {
private const val CRYPTO_TRANSFORMATION = "AES/CBC/PKCS5PADDING"
private const val CRYPTO_ALGORITHM = "AES"
private const val IV_BYTE_LENGTH = 16
// we will use this when we want to sign over an encrypted transaction to attest that we've verified it
private val enclaveKeyPair = Crypto.generateKeyPair()
// this key we will use internally for encryption/decryption n.b. this is JUST for a quick PoC, as we will lose this key on restart
private val encryptionKey = KeyGenerator
.getInstance(CRYPTO_ALGORITHM)
.also { it.init(256) }
.generateKey()
private fun generateRandomBytes(length: Int): ByteArray {
val arr = ByteArray(length)
SecureRandom().nextBytes(arr)
return arr
}
fun getPublicEnclaveKey() = enclaveKeyPair.public
}
fun getDependencies(encryptedTransaction: EncryptedTransaction): Set<SecureHash> {
val stx = decryptTransaction(encryptedTransaction)
return stx.dependencies
}
fun getAttachmentIds(encryptedTransaction: EncryptedTransaction): Set<SecureHash> {
val stx = decryptTransaction(encryptedTransaction)
return stx.tx.attachments.toSet()
}
fun getNetworkParameterHash(encryptedTransaction: EncryptedTransaction): SecureHash? {
val stx = decryptTransaction(encryptedTransaction)
return stx.tx.networkParametersHash
}
// TODO: this is glossing over a lot of difficulty here. toLedgerTransaction resolves a lot of stuff via services which wont be available within an enclave
fun verifyTransaction(encryptedTransaction: EncryptedTransaction, serviceHub : ServiceHub, checkSufficientSignatures: Boolean, rawDependencies: RawDependencyMap): ByteArray {
println("Verifying encrypted ${encryptedTransaction.id} ${serviceHub.myInfo.legalIdentities.single()}")
val stx = decryptTransaction(encryptedTransaction)
val dependencies = extractDependencies(stx.inputs + stx.references, rawDependencies)
// will throw if cannot verify
stx.toLedgerTransaction(serviceHub, checkSufficientSignatures, dependencies).verify()
return Crypto.doSign(enclaveKeyPair.private, stx.txBits.bytes)
}
fun verifyTransaction(signedTransaction: SignedTransaction, serviceHub: ServiceHub, checkSufficientSignatures: Boolean, rawDependencies: RawDependencyMap) {
println("Verifying ${signedTransaction.id} ${serviceHub.myInfo.legalIdentities.single()}")
val dependencies = extractDependencies(signedTransaction.inputs + signedTransaction.references, rawDependencies)
// will throw if cannot verify
signedTransaction.toLedgerTransaction(serviceHub, checkSufficientSignatures, dependencies).verify()
}
fun encryptTransaction(signedTransaction: SignedTransaction): EncryptedTransaction {
println("Encrypting ${signedTransaction.id} with key: $encryptionKey ${encryptionKey.encoded.toHexString()}")
val ivBytes = generateRandomBytes(IV_BYTE_LENGTH)
val initialisationVector = IvParameterSpec(ivBytes)
val encryptionCipher = Cipher
.getInstance(CRYPTO_TRANSFORMATION)
.also { it.init(Cipher.ENCRYPT_MODE, encryptionKey, initialisationVector) }
val encryptedTxBytes = encryptionCipher.doFinal(ivBytes + signedTransaction.serialize().bytes)
return EncryptedTransaction(signedTransaction.id, encryptedTxBytes)
}
private fun decryptTransaction(encryptedTransaction: EncryptedTransaction): SignedTransaction {
val encryptedBytes = encryptedTransaction.bytes
// first IV_BYTE_LENGTH bytes are the IV
val initialisationVector = IvParameterSpec(encryptedTransaction.bytes.copyOf(IV_BYTE_LENGTH))
// remainder is the transaction
val encryptedTransactionBytes = encryptedTransaction.bytes.copyOfRange(IV_BYTE_LENGTH, encryptedBytes.size)
val decryptionCipher = Cipher
.getInstance(CRYPTO_TRANSFORMATION)
.also { it.init(Cipher.DECRYPT_MODE, encryptionKey, initialisationVector) }
println("Decrypting ${encryptedTransaction.id} with key: $encryptionKey ${encryptionKey.encoded.toHexString()}")
return decryptionCipher.doFinal(encryptedTransactionBytes).deserialize()
}
private fun extractDependencies(requiredStateRefs: List<StateRef>, rawDependencies: RawDependencyMap) : SignedTransactionDependencyMap {
val requiredStates = requiredStateRefs.map { it.txhash }.distinct()
val dependencies = requiredStateRefs.map {
val rawDependency = rawDependencies[it.txhash] ?: throw IllegalArgumentException("Missing raw dependency for ${it.txhash}")
val tx = rawDependency.getTransaction() ?: throw IllegalArgumentException("Missing raw dependency data for ${it.txhash} $rawDependency")
it to tx.coreTransaction.outputs[it.index]
}.groupBy { it.first.txhash }
return requiredStates.map {
val resolvedDependencies = dependencies[it] ?: throw IllegalArgumentException("Missing encrypted transaction resolved reference for $it")
it to SignedTransactionDependencies(
inputsAndRefs = resolvedDependencies.toMap(),
networkParameters = rawDependencies[it]?.networkParameters
)
}.toMap()
}
private fun RawDependency.getTransaction() : SignedTransaction? {
return signedTransaction ?: encryptedTransaction?.let { decryptTransaction(it) }
}
}

View File

@ -5,7 +5,9 @@ import net.corda.core.DoNotImplement
import net.corda.core.concurrent.CordaFuture
import net.corda.core.crypto.SecureHash
import net.corda.core.messaging.DataFeed
import net.corda.core.transactions.EncryptedTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.debug
import rx.Observable
/**
@ -19,6 +21,11 @@ interface TransactionStorage {
*/
fun getTransaction(id: SecureHash): SignedTransaction?
/**
* Return the encrypted transaction with the given [id], or null if no such transaction exists.
*/
fun getEncryptedTransaction(id: SecureHash): EncryptedTransaction?
/**
* Get a synchronous Observable of updates. When observations are pushed to the Observer, the vault will already
* incorporate the update.

View File

@ -0,0 +1,29 @@
package net.corda.core.transactions
import net.corda.core.contracts.NamedByHash
import net.corda.core.crypto.SecureHash
import net.corda.core.serialization.CordaSerializable
@CordaSerializable
data class EncryptedTransaction (
override val id : SecureHash,
val bytes : ByteArray
// TODO: will need to also store the signature of who verified this tx
) : NamedByHash{
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as EncryptedTransaction
if (id != other.id) return false
if (!bytes.contentEquals(other.bytes)) return false
return true
}
override fun hashCode(): Int {
return 31 * (id.hashCode() + bytes.contentHashCode())
}
}

View File

@ -10,6 +10,7 @@ import net.corda.core.identity.Party
import net.corda.core.internal.TransactionDeserialisationException
import net.corda.core.internal.TransactionVerifierServiceInternal
import net.corda.core.internal.VisibleForTesting
import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution
import net.corda.core.serialization.CordaSerializable
@ -363,6 +364,47 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
class SignaturesMissingException(val missing: Set<PublicKey>, val descriptions: List<String>, override val id: SecureHash)
: NamedByHash, SignatureException(missingSignatureMsg(missing, descriptions, id)), CordaThrowable by CordaException(missingSignatureMsg(missing, descriptions, id))
//region Added for encryption PoC
@JvmOverloads
@DeleteForDJVM
@Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class)
fun toLedgerTransaction(services: ServiceHub, checkSufficientSignatures: Boolean = true, dependencyMap: SignedTransactionDependencyMap): LedgerTransaction {
if (checkSufficientSignatures) {
verifyRequiredSignatures() // It internally invokes checkSignaturesAreValid().
} else {
checkSignaturesAreValid()
}
// We need parameters check here, because finality flow calls stx.toLedgerTransaction() and then verify.
resolveAndCheckNetworkParameters(services, dependencyMap)
return tx.toLedgerTransaction(services, dependencyMap)
}
@DeleteForDJVM
private fun resolveAndCheckNetworkParameters(services: ServiceHub, dependencyMap: SignedTransactionDependencyMap) {
val defaultNetworkParameters = services.networkParametersService.lookup(services.networkParametersService.defaultHash)
val txNetworkParameters = if (networkParametersHash != null) {
services.networkParametersService.lookup(networkParametersHash!!)
} else {
defaultNetworkParameters
} ?: throw TransactionResolutionException(id)
val groupedInputsAndRefs = (inputs + references).groupBy { it.txhash }
groupedInputsAndRefs.map { entry ->
val dependencies = dependencyMap[entry.key] ?: throw TransactionResolutionException(id)
val params = (dependencies.networkParameters ?: defaultNetworkParameters) ?: throw TransactionResolutionException(id)
if (txNetworkParameters.epoch < params.epoch) {
throw TransactionVerificationException.TransactionNetworkParameterOrderingException(id, entry.value.first(), txNetworkParameters, params)
}
}
}
//endregion
//region Deprecated
/** Returns the contained [NotaryChangeWireTransaction], or throws if this is a normal transaction. */
@Deprecated("No replacement, this should not be used outside of Corda core")
@ -373,3 +415,25 @@ data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
fun isNotaryChangeTransaction() = this.coreTransaction is NotaryChangeWireTransaction
//endregion
}
typealias SignedTransactionDependencyMap = Map<SecureHash, SignedTransactionDependencies>
data class SignedTransactionDependencies(
val inputsAndRefs : Map<StateRef, TransactionState<ContractState>>,
val networkParameters : NetworkParameters?
)
//data class RawDependencies(
// val encryptedTransactions: Map<SecureHash, EncryptedTransaction>,
// val signedTransactions: Map<SecureHash, SignedTransaction>
// val networkParameters: NetworkParameters
//)
typealias RawDependencyMap = Map<SecureHash, RawDependency>
data class RawDependency(
val encryptedTransaction: EncryptedTransaction?,
val signedTransaction: SignedTransaction?,
val networkParameters: NetworkParameters?
)

View File

@ -123,6 +123,31 @@ class WireTransaction(componentGroups: List<ComponentGroup>, val privacySalt: Pr
)
}
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
@DeleteForDJVM
fun toLedgerTransaction(services: ServicesForResolution, dependencyMap: SignedTransactionDependencyMap): LedgerTransaction {
return services.specialise(
toLedgerTransactionInternal(
resolveIdentity = { services.identityService.partyFromKey(it) },
resolveAttachment = { services.attachments.openAttachment(it) },
resolveStateRefAsSerialized = {
val dependencies = dependencyMap[it.txhash] ?: throw TransactionResolutionException(it.txhash)
val stateRef = dependencies.inputsAndRefs[it] ?: throw TransactionResolutionException(it.txhash)
stateRef.serialize()
},
resolveParameters = {
val hashToResolve = it ?: services.networkParametersService.defaultHash
services.networkParametersService.lookup(hashToResolve)
},
// `as?` is used due to [MockServices] not implementing [ServiceHubCoreInternal]
isAttachmentTrusted = { (services as? ServiceHubCoreInternal)?.attachmentTrustCalculator?.calculate(it) ?: true },
attachmentsClassLoaderCache = (services as? ServiceHubCoreInternal)?.attachmentsClassLoaderCache
)
)
}
// Helper for deprecated toLedgerTransaction
// TODO: revisit once Deterministic JVM code updated
@Suppress("UNUSED") // not sure if this field can be removed safely??

View File

@ -4,8 +4,10 @@ import net.corda.core.identity.Party
import net.corda.core.node.services.Vault
import net.corda.core.node.services.trackBy
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.getOrThrow
import net.corda.core.utilities.toHexString
import net.corda.finance.DOLLARS
import net.corda.finance.`issued by`
import net.corda.finance.contracts.asset.Cash
@ -29,6 +31,9 @@ class CashPaymentFlowTests {
private lateinit var bankOfCordaNode: StartedMockNode
private lateinit var bankOfCorda: Party
private lateinit var aliceNode: StartedMockNode
private lateinit var bobNode: StartedMockNode
private lateinit var issuanceTx: SignedTransaction
@Before
fun start() {
@ -36,8 +41,9 @@ class CashPaymentFlowTests {
bankOfCordaNode = mockNet.createPartyNode(BOC_NAME)
bankOfCorda = bankOfCordaNode.info.identityFromX500Name(BOC_NAME)
aliceNode = mockNet.createPartyNode(ALICE_NAME)
bobNode = mockNet.createPartyNode(BOB_NAME)
val future = bankOfCordaNode.startFlow(CashIssueFlow(initialBalance, ref, mockNet.defaultNotaryIdentity))
future.getOrThrow()
issuanceTx = future.getOrThrow().stx
}
@After
@ -58,7 +64,12 @@ class CashPaymentFlowTests {
val future = bankOfCordaNode.startFlow(CashPaymentFlow(expectedPayment, payTo))
mockNet.runNetwork()
future.getOrThrow()
val payTx = future.getOrThrow().stx
// pay Bob (not anonymously as we want to check that Bob owns it)
val futureBob = aliceNode.startFlow(CashPaymentFlow(expectedPayment, bobNode.info.singleIdentity(), false))
mockNet.runNetwork()
val bobTx = futureBob.getOrThrow().stx
// Check Bank of Corda vault updates - we take in some issued cash and split it into $500 to the notary
// and $1,500 back to us, so we expect to consume one state, produce one state for our own vault
@ -80,6 +91,18 @@ class CashPaymentFlowTests {
assertEquals(expectedPayment.`issued by`(bankOfCorda.ref(ref)), paymentState.amount)
}
}
listOf(bobNode, aliceNode, bankOfCordaNode).forEach { node ->
listOf(issuanceTx, payTx, bobTx).forEach { stx ->
println("${node.info.singleIdentity()} UNENCRYPTED: ${node.services.validatedTransactions.getTransaction(stx.id)}")
println("${node.info.singleIdentity()} ENCRYPTED: ${node.services.validatedTransactions.getEncryptedTransaction(stx.id)?.let { "${stx.id} -> ${it.bytes.toHexString()}"}}")
}
}
bobNode.services.vaultService.queryBy(Cash.State::class.java).states.forEach {
println("BOB: ${it.state.data}")
}
}
@Test(timeout=300_000)

View File

@ -53,6 +53,7 @@ import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.ContractUpgradeService
import net.corda.core.node.services.CordaService
import net.corda.core.node.services.EncryptedTransactionService
import net.corda.core.node.services.IdentityService
import net.corda.core.node.services.KeyManagementService
import net.corda.core.node.services.TransactionVerifierService
@ -290,7 +291,10 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
val pkToIdCache = PublicKeyToOwningIdentityCacheImpl(database, cacheFactory)
@Suppress("LeakingThis")
val keyManagementService = makeKeyManagementService(identityService).tokenize()
val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersStorage, transactionStorage).also {
val encryptedTransactionService = EncryptedTransactionService().tokenize()
val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersStorage, transactionStorage, encryptedTransactionService).also {
attachments.servicesForResolution = it
}
@Suppress("LeakingThis")
@ -1134,6 +1138,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
override val diagnosticsService: DiagnosticsService get() = this@AbstractNode.diagnosticsService
override val externalOperationExecutor: ExecutorService get() = this@AbstractNode.externalOperationExecutor
override val notaryService: NotaryService? get() = this@AbstractNode.notaryService
override val encryptedTransactionService: EncryptedTransactionService = this@AbstractNode.encryptedTransactionService
private lateinit var _myInfo: NodeInfo
override val myInfo: NodeInfo get() = _myInfo

View File

@ -6,6 +6,7 @@ import net.corda.core.internal.SerializedStateAndRef
import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentStorage
import net.corda.core.node.services.EncryptedTransactionService
import net.corda.core.node.services.IdentityService
import net.corda.core.node.services.NetworkParametersService
import net.corda.core.node.services.TransactionStorage
@ -19,7 +20,8 @@ data class ServicesForResolutionImpl(
override val attachments: AttachmentStorage,
override val cordappProvider: CordappProvider,
override val networkParametersService: NetworkParametersService,
private val validatedTransactions: TransactionStorage
private val validatedTransactions: TransactionStorage,
override val encryptedTransactionService: EncryptedTransactionService
) : ServicesForResolution {
override val networkParameters: NetworkParameters get() = networkParametersService.lookup(networkParametersService.currentHash) ?:
throw IllegalArgumentException("No current parameters in network parameters storage")

View File

@ -10,6 +10,7 @@ import net.corda.core.internal.readObject
import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServicesForResolution
import net.corda.core.node.services.AttachmentId
import net.corda.core.node.services.EncryptedTransactionService
import net.corda.core.node.services.IdentityService
import net.corda.core.node.services.NetworkParametersService
import net.corda.core.node.services.TransactionStorage
@ -38,7 +39,8 @@ class MigrationServicesForResolution(
override val attachments: AttachmentStorageInternal,
private val transactions: TransactionStorage,
private val cordaDB: CordaPersistence,
cacheFactory: MigrationNamedCacheFactory
cacheFactory: MigrationNamedCacheFactory,
override val encryptedTransactionService: EncryptedTransactionService = EncryptedTransactionService()
): ServicesForResolution {
companion object {

View File

@ -3,11 +3,14 @@ package net.corda.node.services
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
import net.corda.core.internal.FetchEncryptedTransactionsFlow
import net.corda.core.internal.FetchTransactionsFlow
import net.corda.core.internal.ResolveTransactionsFlow
import net.corda.core.internal.TransactionsResolver
import net.corda.core.internal.dependencies
import net.corda.core.node.StatesToRecord
import net.corda.core.transactions.EncryptedTransaction
import net.corda.core.transactions.RawDependency
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.debug
import net.corda.core.utilities.trace
@ -113,6 +116,131 @@ class DbTransactionsResolver(private val flow: ResolveTransactionsFlow) : Transa
}
}
@Suspendable
override fun downloadEncryptedDependencies() {
logger.debug { "Downloading encrypted dependencies for transactions ${flow.txHashes}" }
val transactionStorage = flow.serviceHub.validatedTransactions as WritableTransactionStorage
val encryptSvc = flow.serviceHub.encryptedTransactionService
val nextRequests = LinkedHashSet<SecureHash>(flow.txHashes) // Keep things unique but ordered, for unit test stability.
val topologicalSort = TopologicalSort()
while (nextRequests.isNotEmpty()) {
logger.debug { "Main fetch loop: size_remaining=${nextRequests.size}" }
// Don't re-download the same tx when we haven't verified it yet but it's referenced multiple times in the
// graph we're traversing.
nextRequests.removeAll(topologicalSort.transactionIds)
if (nextRequests.isEmpty()) {
// Done early.
break
}
// Request the standalone transaction data (which may refer to things we don't yet have).
val (existingTxIds, downloadedTxs) = fetchEncryptedRequiredTransactions(Collections.singleton(nextRequests.first())) // Fetch first item only
for (tx in downloadedTxs) {
val dependencies = encryptSvc.getDependencies(tx)
topologicalSort.add(tx.id, dependencies)
}
var suspended = true
for (downloaded in downloadedTxs) {
suspended = false
val dependencies = encryptSvc.getDependencies(downloaded)
// Do not keep in memory as this bloats the checkpoint. Write each item to the database.
transactionStorage.addUnverifiedEncryptedTransaction(downloaded)
// The write locks are only released over a suspend, so need to keep track of whether the flow has been suspended to ensure
// that locks are not held beyond each while loop iteration (as doing this would result in a deadlock due to claiming locks
// in the wrong order)
val suspendedViaAttachments = flow.fetchMissingAttachments(downloaded)
val suspendedViaParams = flow.fetchMissingNetworkParameters(downloaded)
suspended = suspended || suspendedViaAttachments || suspendedViaParams
// Add all input states and reference input states to the work queue.
nextRequests.addAll(dependencies)
}
// If the flow did not suspend on the last iteration of the downloaded loop above, perform a suspend here to ensure that
// all data is flushed to the database.
if (!suspended) {
FlowLogic.sleep(0.seconds)
}
// It's possible that the node has a transaction in storage already. Dependencies should also be present for this transaction,
// so just remove these IDs from the set of next requests.
nextRequests.removeAll(existingTxIds)
}
sortedDependencies = topologicalSort.complete()
logger.debug { "Downloaded ${sortedDependencies?.size} dependencies from remote peer for transactions ${flow.txHashes}" }
}
@Suspendable
override fun recordEncryptedDependencies(usedStatesToRecord: StatesToRecord) {
val sortedDependencies = checkNotNull(this.sortedDependencies)
val encryptSvc = flow.serviceHub.encryptedTransactionService
logger.trace { "Recording ${sortedDependencies.size} dependencies for ${flow.txHashes.size} transactions" }
val transactionStorage = flow.serviceHub.validatedTransactions as WritableTransactionStorage
for (txId in sortedDependencies) {
// Retrieve and delete the transaction from the unverified store.
val (tx, isVerified) = checkNotNull(transactionStorage.getEncryptedTransactionInternal(txId)) {
"Somehow the unverified transaction ($txId) that we stored previously is no longer there."
}
if (!isVerified) {
val dependencies = encryptSvc.getDependencies(tx)
val encryptedTxs = dependencies.mapNotNull {
depTxId ->
transactionStorage.getEncryptedTransaction(depTxId)?.let { etx ->
etx.id to etx
}
}.toMap()
val signedTxs = dependencies.mapNotNull {
depTxId ->
transactionStorage.getTransaction(depTxId)?.let { stx ->
stx.id to stx
}
}.toMap()
val services = flow.serviceHub
val networkParameters = dependencies.mapNotNull { depTxId ->
val npHash = when {
encryptedTxs[depTxId] != null -> encryptSvc.getNetworkParameterHash(encryptedTxs[depTxId]!!)
?: services.networkParametersService.defaultHash
signedTxs[depTxId] != null -> signedTxs[depTxId]!!.networkParametersHash
?: services.networkParametersService.defaultHash
else -> null
}
npHash?.let { depTxId to npHash }
}.associate {
it.first to services.networkParametersService.lookup(it.second)
}
val rawDependencies = dependencies.associate {
it to RawDependency(
encryptedTxs[it],
signedTxs[it],
networkParameters[it]
)
}
encryptSvc.verifyTransaction(tx, flow.serviceHub, true, rawDependencies)
// TODO: why does this usually go through the serviceHub's recordTransactions function and not
// direct to the validatedTransactions service??
// flow.serviceHub.recordTransactions(usedStatesToRecord, listOf(tx))
val transactionStorage = flow.serviceHub.validatedTransactions as WritableTransactionStorage
transactionStorage.addEncryptedTransaction(tx)
} else {
logger.debug { "No need to record $txId as it's already been verified" }
}
}
}
// The transactions already present in the database do not need to be checkpointed on every iteration of downloading
// dependencies for other transactions, so strip these down to just the IDs here.
@Suspendable
@ -121,6 +249,12 @@ class DbTransactionsResolver(private val flow: ResolveTransactionsFlow) : Transa
return Pair(requestedTxs.fromDisk.map { it.id }, requestedTxs.downloaded)
}
@Suspendable
private fun fetchEncryptedRequiredTransactions(requests: Set<SecureHash>): Pair<List<SecureHash>, List<EncryptedTransaction>> {
val requestedTxs = flow.subFlow(FetchEncryptedTransactionsFlow(requests, flow.otherSide))
return Pair(requestedTxs.fromDisk.map { it.id }, requestedTxs.downloaded)
}
/**
* Provides a way to topologically sort SignedTransactions represented just their [SecureHash] IDs. This means that given any two transactions
* T1 and T2 in the list returned by [complete] if T1 is a dependency of T2 then T1 will occur earlier than T2.

View File

@ -14,8 +14,10 @@ import net.corda.core.node.StatesToRecord
import net.corda.core.node.services.NetworkMapCache
import net.corda.core.node.services.NetworkMapCacheBase
import net.corda.core.node.services.TransactionStorage
import net.corda.core.transactions.EncryptedTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
import net.corda.node.internal.InitiatedFlowFactory
import net.corda.node.internal.cordapp.CordappProviderInternal
import net.corda.node.services.DbTransactionsResolver
@ -23,6 +25,7 @@ import net.corda.node.services.config.NodeConfiguration
import net.corda.node.services.messaging.MessagingService
import net.corda.node.services.network.NetworkMapUpdater
import net.corda.node.services.persistence.AttachmentStorageInternal
import net.corda.node.services.persistence.DBTransactionStorage
import net.corda.node.services.statemachine.ExternalEvent
import net.corda.node.services.statemachine.FlowStateMachineImpl
import net.corda.nodeapi.internal.persistence.CordaPersistence
@ -253,17 +256,33 @@ interface WritableTransactionStorage : TransactionStorage {
// TODO: Throw an exception if trying to add a transaction with fewer signatures than an existing entry.
fun addTransaction(transaction: SignedTransaction): Boolean
/**
* Add a new encrypted transaction to the store
*/
fun addEncryptedTransaction(encryptedTransaction: EncryptedTransaction): Boolean
/**
* Add a new *unverified* transaction to the store.
*/
fun addUnverifiedTransaction(transaction: SignedTransaction)
/**
* Add a new *unverified* encrypted transaction to the store.
*/
fun addUnverifiedEncryptedTransaction(encryptedTransaction: EncryptedTransaction)
/**
* Return the transaction with the given ID from the store, and a flag of whether it's verified. Returns null if no transaction with the
* ID exists.
*/
fun getTransactionInternal(id: SecureHash): Pair<SignedTransaction, Boolean>?
/**
* Return the transaction with the given ID from the store, and a flag of whether it's verified. Returns null if no transaction with the
* ID exists.
*/
fun getEncryptedTransactionInternal(id: SecureHash): Pair<EncryptedTransaction, Boolean>?
/**
* Returns a future that completes with the transaction corresponding to [id] once it has been committed. Do not warn when run inside
* a DB transaction.

View File

@ -13,6 +13,7 @@ import net.corda.core.serialization.*
import net.corda.core.serialization.internal.effectiveSerializationEnv
import net.corda.core.toFuture
import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.EncryptedTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.contextLogger
import net.corda.core.utilities.debug
@ -53,7 +54,12 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory:
val status: TransactionStatus,
@Column(name = "timestamp", nullable = false)
val timestamp: Instant
val timestamp: Instant,
@Column(name = "encrypted", nullable = false)
val encrypted: Boolean = false
// TODO: will need to also store the signature of who verified this tx
)
enum class TransactionStatus {
@ -120,17 +126,33 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory:
name = "DBTransactionStorage_transactions",
toPersistentEntityKey = SecureHash::toString,
fromPersistentEntity = {
if (it.encrypted) {
SecureHash.create(it.txId) to TxCacheValue(
it.transaction.deserialize(context = contextToUse()),
it.status)
EncryptedTransaction(
SecureHash.parse(it.txId),
it.transaction
),
it.status
)
} else {
SecureHash.create(it.txId) to TxCacheValue(
it.transaction.deserialize<SignedTransaction>(context = contextToUse()),
it.status
)
}
},
toPersistentEntity = { key: SecureHash, value: TxCacheValue ->
DBTransaction(
txId = key.toString(),
stateMachineRunId = FlowStateMachineImpl.currentStateMachine()?.id?.uuid?.toString(),
transaction = value.toSignedTx().serialize(context = contextToUse().withEncoding(SNAPPY)).bytes,
transaction = if( value.encrypted ) {
value.txBits
} else {
value.toSignedTx().serialize(context = contextToUse().withEncoding(SNAPPY)).bytes
},
status = value.status,
timestamp = clock.instant()
timestamp = clock.instant(),
encrypted = value.encrypted
)
},
persistentEntityClass = DBTransaction::class.java,
@ -138,6 +160,7 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory:
)
}
// TODO: weight of transactions will be wrong at this stage for encrypted transactions
private fun weighTx(tx: AppendOnlyPersistentMapBase.Transactional<TxCacheValue>): Int {
val actTx = tx.peekableValue ?: return 0
return actTx.sigs.sumBy { it.size + transactionSignatureOverheadEstimate } + actTx.txBits.size
@ -187,7 +210,7 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory:
override fun getTransaction(id: SecureHash): SignedTransaction? {
return database.transaction {
txStorage.content[id]?.let { if (it.status.isVerified()) it.toSignedTx() else null }
txStorage.content[id]?.let { if (it.status.isVerified() && !it.encrypted ) it.toSignedTx() else null }
}
}
@ -207,7 +230,58 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory:
override fun getTransactionInternal(id: SecureHash): Pair<SignedTransaction, Boolean>? {
return database.transaction {
txStorage.content[id]?.let { it.toSignedTx() to it.status.isVerified() }
txStorage.content[id]?.let {
if (!it.encrypted) {
it.toSignedTx() to it.status.isVerified()
} else null
}
}
}
override fun addEncryptedTransaction(encryptedTransaction: EncryptedTransaction): Boolean {
val transactionId = encryptedTransaction.id
return database.transaction {
txStorage.locked {
val cachedValue = TxCacheValue(encryptedTransaction, TransactionStatus.VERIFIED)
val addedOrUpdated = addOrUpdate(transactionId, cachedValue) { k, _ -> updateTransaction(k) }
if (addedOrUpdated) {
logger.debug { "Transaction $transactionId has been recorded as verified" }
} else {
logger.debug { "Transaction $transactionId is already recorded as verified, so no need to re-record" }
}
addedOrUpdated
}
}
}
override fun getEncryptedTransaction(id: SecureHash): EncryptedTransaction? {
return database.transaction {
txStorage.content[id]?.let { if (it.status.isVerified() && it.encrypted ) it.toEncryptedTx() else null }
}
}
override fun addUnverifiedEncryptedTransaction(encryptedTransaction: EncryptedTransaction) {
val transactionId = encryptedTransaction.id
database.transaction {
txStorage.locked {
val cacheValue = TxCacheValue(encryptedTransaction, status = TransactionStatus.UNVERIFIED)
val added = addWithDuplicatesAllowed(transactionId, cacheValue)
if (added) {
logger.debug { "Encrypted Transaction $transactionId recorded as unverified." }
} else {
logger.info("Encrypted Transaction $transactionId already exists so no need to record.")
}
}
}
}
override fun getEncryptedTransactionInternal(id: SecureHash): Pair<EncryptedTransaction, Boolean>? {
return database.transaction {
txStorage.content[id]?.let {
if (it.encrypted) {
it.toEncryptedTx() to it.status.isVerified()
} else null
}
}
}
@ -262,21 +336,70 @@ class DBTransactionStorage(private val database: CordaPersistence, cacheFactory:
private fun snapshot(): List<SignedTransaction> {
return txStorage.content.allPersisted.use {
it.filter { it.second.status.isVerified() }.map { it.second.toSignedTx() }.toList()
it.filter { it.second.status.isVerified() && !it.second.encrypted }.map { it.second.toSignedTx() }.toList()
}
}
// Cache value type to just store the immutable bits of a signed transaction plus conversion helpers
private data class TxCacheValue(
val txBits: SerializedBytes<CoreTransaction>,
val id: SecureHash,
val txBits: ByteArray,
val sigs: List<TransactionSignature>,
val status: TransactionStatus
val status: TransactionStatus,
val encrypted: Boolean
) {
constructor(stx: SignedTransaction, status: TransactionStatus) : this(
stx.txBits,
Collections.unmodifiableList(stx.sigs),
status)
stx.id,
stx.txBits.bytes,
stx.sigs,
status,
false)
fun toSignedTx() = SignedTransaction(txBits, sigs)
constructor(encryptedTransaction: EncryptedTransaction, status: TransactionStatus) : this(
encryptedTransaction.id,
encryptedTransaction.bytes,
emptyList(),
status,
true)
fun toSignedTx() : SignedTransaction {
return if (!encrypted) {
val txBitsAsSerialized = SerializedBytes<CoreTransaction>(txBits)
SignedTransaction(txBitsAsSerialized, sigs)
} else {
throw IllegalArgumentException("Cannot get signed transaction for encrypted tx")
}
}
fun toEncryptedTx() : EncryptedTransaction {
return if (encrypted) {
// TODO: EncryptedTransaction will be extended to include verification signature
EncryptedTransaction(id, txBits)
} else {
throw IllegalArgumentException("Cannot get encrypted transaction for signed tx")
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as TxCacheValue
if (!txBits.contentEquals(other.txBits)) return false
if (sigs != other.sigs) return false
if (status != other.status) return false
if (encrypted != other.encrypted) return false
return true
}
override fun hashCode(): Int {
var result = txBits.contentHashCode()
result = 31 * result + sigs.hashCode()
result = 31 * result + status.hashCode()
result = 31 * result + encrypted.hashCode()
return result
}
}
}

View File

@ -0,0 +1,14 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"
logicalFilePath="migration/node-services.changelog-init.xml">
<changeSet author="R3.Corda" id="add_encrypted_column_to_node_transactions">
<addColumn tableName="node_transactions">
<column name="encrypted" type="BOOLEAN" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
</databaseChangeLog>

View File

@ -18,6 +18,7 @@ import net.corda.core.node.services.Vault
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.toFuture
import net.corda.core.transactions.EncryptedTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
@ -786,6 +787,31 @@ class TwoPartyTradeFlowTests(private val anonymous: Boolean) {
delegate.getTransactionInternal(id)
}
}
// TODO: these Encrypted transactions may need an overhaul is probably indicative that overloading the current storage was a bad idea
override fun addEncryptedTransaction(encryptedTransaction: EncryptedTransaction): Boolean {
return database.transaction {
delegate.addEncryptedTransaction(encryptedTransaction)
}
}
override fun addUnverifiedEncryptedTransaction(encryptedTransaction: EncryptedTransaction) {
return database.transaction {
delegate.addUnverifiedEncryptedTransaction(encryptedTransaction)
}
}
override fun getEncryptedTransaction(id: SecureHash): EncryptedTransaction? {
return database.transaction {
delegate.getEncryptedTransaction(id)
}
}
override fun getEncryptedTransactionInternal(id: SecureHash): Pair<EncryptedTransaction, Boolean>? {
return database.transaction {
delegate.getEncryptedTransactionInternal(id)
}
}
}
interface TxRecord {

View File

@ -7,7 +7,13 @@ import net.corda.core.crypto.Crypto
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignatureMetadata
import net.corda.core.crypto.TransactionSignature
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.internal.effectiveSerializationEnv
import net.corda.core.serialization.serialize
import net.corda.core.toFuture
import net.corda.core.transactions.EncryptedTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.node.CordaClock
import net.corda.node.MutableClock
@ -15,6 +21,7 @@ import net.corda.node.SimpleClock
import net.corda.node.services.transactions.PersistentUniquenessProvider
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.serialization.internal.CordaSerializationEncoding
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.SerializationEnvironmentRule
@ -32,12 +39,17 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import rx.plugins.RxJavaHooks
import java.security.SecureRandom
import java.time.Clock
import java.time.Instant
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.spec.IvParameterSpec
import kotlin.concurrent.thread
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
class DBTransactionStorageTests {
private companion object {
@ -390,6 +402,47 @@ class DBTransactionStorageTests {
assertThat(warning).isEqualTo(DBTransactionStorage.TRANSACTION_ALREADY_IN_PROGRESS_WARNING)
}
@Test(timeout=300_000)
fun `encrypted transaction`() {
val now = Instant.ofEpochSecond(111222333L)
val transactionClock = TransactionClock(now)
newTransactionStorage(clock = transactionClock)
val transaction = newTransaction()
val keygen = KeyGenerator.getInstance("AES")
keygen.init(256)
val key = keygen.generateKey()
val cipherTransformation = "AES/CBC/PKCS5PADDING"
val encryptionCipher = Cipher.getInstance(cipherTransformation)
val iv = generateIv()
encryptionCipher.init(Cipher.ENCRYPT_MODE, key, iv)
val encryptedTxBytes = encryptionCipher.doFinal(transaction.serialize(context = contextToUse().withEncoding(CordaSerializationEncoding.SNAPPY)).bytes)
val encryptedTx = EncryptedTransaction(transaction.id, encryptedTxBytes)
transactionStorage.addEncryptedTransaction(encryptedTx)
val storedTx = transactionStorage.getEncryptedTransaction(transaction.id)
val decryptionCipher = Cipher.getInstance(cipherTransformation)
decryptionCipher.init(Cipher.DECRYPT_MODE, key, iv)
assertNotNull(storedTx, "Could not find stored encrypted message")
val decryptedTx = decryptionCipher.doFinal(storedTx!!.bytes).deserialize<SignedTransaction>(context = contextToUse())
assertEquals(decryptedTx, transaction)
assertEquals(now, readTransactionTimestampFromDB(transaction.id))
}
fun generateIv(): IvParameterSpec? {
val iv = ByteArray(16)
SecureRandom().nextBytes(iv)
return IvParameterSpec(iv)
}
private fun newTransactionStorage(cacheSizeBytesOverride: Long? = null, clock: CordaClock = SimpleClock(Clock.systemUTC())) {
transactionStorage = DBTransactionStorage(database, TestingNamedCacheFactory(cacheSizeBytesOverride
?: 1024), clock)
@ -413,4 +466,12 @@ class DBTransactionStorageTests {
listOf(TransactionSignature(ByteArray(1), ALICE_PUBKEY, SignatureMetadata(1, Crypto.findSignatureScheme(ALICE_PUBKEY).schemeNumberID)))
)
}
private fun contextToUse(): SerializationContext {
return if (effectiveSerializationEnv.serializationFactory.currentContext?.useCase == SerializationContext.UseCase.Storage) {
effectiveSerializationEnv.serializationFactory.currentContext!!
} else {
SerializationDefaults.STORAGE_CONTEXT
}
}
}

View File

@ -86,7 +86,8 @@ open class MockServices private constructor(
override val keyManagementService: KeyManagementService = MockKeyManagementService(
identityService,
*arrayOf(initialIdentity.keyPair) + moreKeys
)
),
override val encryptedTransactionService : EncryptedTransactionService = EncryptedTransactionService()
) : ServiceHub {
companion object {
@ -457,7 +458,7 @@ open class MockServices private constructor(
override val diagnosticsService: DiagnosticsService = NodeDiagnosticsService()
protected val servicesForResolution: ServicesForResolution
get() = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersService, validatedTransactions)
get() = ServicesForResolutionImpl(identityService, attachments, cordappProvider, networkParametersService, validatedTransactions, encryptedTransactionService)
internal fun makeVaultService(schemaService: SchemaService, database: CordaPersistence, cordappLoader: CordappLoader): VaultServiceInternal {
return NodeVaultService(clock, keyManagementService, servicesForResolution, database, schemaService, cordappLoader.appClassLoader).apply { start() }

View File

@ -6,6 +6,7 @@ import net.corda.core.internal.concurrent.doneFuture
import net.corda.core.messaging.DataFeed
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.toFuture
import net.corda.core.transactions.EncryptedTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.node.services.api.WritableTransactionStorage
import net.corda.testing.node.MockServices
@ -30,6 +31,7 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali
}
private val txns = HashMap<SecureHash, TxHolder>()
private val encryptedTxns = HashMap<SecureHash, EncryptedTxHolder>()
private val _updatesPublisher = PublishSubject.create<SignedTransaction>()
@ -61,5 +63,27 @@ open class MockTransactionStorage : WritableTransactionStorage, SingletonSeriali
override fun getTransactionInternal(id: SecureHash): Pair<SignedTransaction, Boolean>? = txns[id]?.let { Pair(it.stx, it.isVerified) }
override fun addEncryptedTransaction(encryptedTransaction: EncryptedTransaction): Boolean {
val current = encryptedTxns.putIfAbsent(encryptedTransaction.id, EncryptedTxHolder(encryptedTransaction, isVerified = true))
return if (current == null) {
true
} else if (!current.isVerified) {
current.isVerified = true
true
} else {
false
}
}
override fun addUnverifiedEncryptedTransaction(encryptedTransaction: EncryptedTransaction) {
encryptedTxns.putIfAbsent(encryptedTransaction.id, EncryptedTxHolder(encryptedTransaction, isVerified = false))
}
override fun getEncryptedTransaction(id: SecureHash): EncryptedTransaction? = encryptedTxns[id]?.let { if (it.isVerified) it.etx else null }
override fun getEncryptedTransactionInternal(id: SecureHash): Pair<EncryptedTransaction, Boolean>? =
encryptedTxns[id]?.let { Pair(it.etx, it.isVerified) }
private class TxHolder(val stx: SignedTransaction, var isVerified: Boolean)
private class EncryptedTxHolder(val etx: EncryptedTransaction, var isVerified: Boolean)
}