CORDA-2694: Prevent Node Explorer from crashing should it receive unknown transaction objects. (#4842)

* CORDA-2694: Prevent Node Explorer from crashing should it receive unknown transaction objects.
Also ensure that LazyMappedList can only handle TransactionDeserialisationExceptions.

* CORDA-2694: Add unit tests for eager LazyMappedList behaviour.

* CORDA-2694: Hide LazyMappedList from the client:jfx module.

* CORDA-2694: Create an unknown transaction state that has the correct notary.
This commit is contained in:
Chris Rankin 2019-03-05 08:49:23 +00:00 committed by Tommy Lillehagen
parent cfccfd075e
commit fae74eecde
7 changed files with 89 additions and 12 deletions

View File

@ -119,19 +119,19 @@ class NodeMonitorModel : AutoCloseable {
}.toSet() }.toSet()
val consumedStates = statesSnapshot.states.toSet() - unconsumedStates val consumedStates = statesSnapshot.states.toSet() - unconsumedStates
val initialVaultUpdate = Vault.Update(consumedStates, unconsumedStates, references = emptySet()) val initialVaultUpdate = Vault.Update(consumedStates, unconsumedStates, references = emptySet())
vaultUpdates.startWith(initialVaultUpdate).subscribe({ vaultUpdatesSubject.onNext(it) }, {}) vaultUpdates.startWith(initialVaultUpdate).subscribe(vaultUpdatesSubject::onNext, {})
// Transactions // Transactions
val (transactions, newTransactions) = proxy.internalVerifiedTransactionsFeed() val (transactions, newTransactions) = proxy.internalVerifiedTransactionsFeed()
newTransactions.startWith(transactions).subscribe({ transactionsSubject.onNext(it) }, {}) newTransactions.startWith(transactions).subscribe(transactionsSubject::onNext, {})
// SM -> TX mapping // SM -> TX mapping
val (smTxMappings, futureSmTxMappings) = proxy.stateMachineRecordedTransactionMappingFeed() val (smTxMappings, futureSmTxMappings) = proxy.stateMachineRecordedTransactionMappingFeed()
futureSmTxMappings.startWith(smTxMappings).subscribe({ stateMachineTransactionMappingSubject.onNext(it) }, {}) futureSmTxMappings.startWith(smTxMappings).subscribe(stateMachineTransactionMappingSubject::onNext, {})
// Parties on network // Parties on network
val (parties, futurePartyUpdate) = proxy.networkMapFeed() val (parties, futurePartyUpdate) = proxy.networkMapFeed()
futurePartyUpdate.startWith(parties.map { MapChange.Added(it) }).subscribe({ networkMapSubject.onNext(it) }, {}) futurePartyUpdate.startWith(parties.map(MapChange::Added)).subscribe(networkMapSubject::onNext, {})
} }
} }

View File

@ -5,11 +5,24 @@ import net.corda.client.jfx.utils.distinctBy
import net.corda.client.jfx.utils.lift import net.corda.client.jfx.utils.lift
import net.corda.client.jfx.utils.map import net.corda.client.jfx.utils.map
import net.corda.client.jfx.utils.recordInSequence import net.corda.client.jfx.utils.recordInSequence
import net.corda.core.contracts.ContractState import net.corda.core.contracts.*
import net.corda.core.contracts.StateAndRef import net.corda.core.crypto.entropyToKeyPair
import net.corda.core.contracts.StateRef import net.corda.core.identity.AbstractParty
import net.corda.core.identity.CordaX500Name
import net.corda.core.identity.Party
import net.corda.core.internal.eagerDeserialise
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
import java.math.BigInteger.ZERO
private class Unknown : Contract {
override fun verify(tx: LedgerTransaction) = throw UnsupportedOperationException()
object State : ContractState {
override val participants: List<AbstractParty> = emptyList()
}
}
/** /**
* [PartiallyResolvedTransaction] holds a [SignedTransaction] that has zero or more inputs resolved. The intent is * [PartiallyResolvedTransaction] holds a [SignedTransaction] that has zero or more inputs resolved. The intent is
@ -41,10 +54,24 @@ data class PartiallyResolvedTransaction(
} }
companion object { companion object {
private val DUMMY_NOTARY = Party(CordaX500Name("Dummy Notary", "Nowhere", "ZZ"), entropyToKeyPair(ZERO).public)
fun fromSignedTransaction( fun fromSignedTransaction(
transaction: SignedTransaction, transaction: SignedTransaction,
inputTransactions: Map<StateRef, SignedTransaction?> inputTransactions: Map<StateRef, SignedTransaction?>
): PartiallyResolvedTransaction { ): PartiallyResolvedTransaction {
/**
* Forcibly deserialize our transaction outputs up-front.
* Replace any [TransactionState] objects that fail to
* deserialize with a dummy transaction state that uses
* the transaction's notary.
*/
val unknownTransactionState = TransactionState(
data = Unknown.State,
contract = Unknown::class.java.name,
notary = transaction.notary ?: DUMMY_NOTARY
)
transaction.coreTransaction.outputs.eagerDeserialise { _, _ -> unknownTransactionState }
return PartiallyResolvedTransaction( return PartiallyResolvedTransaction(
transaction = transaction, transaction = transaction,
inputs = transaction.inputs.map { stateRef -> inputs = transaction.inputs.map { stateRef ->

View File

@ -1,7 +1,6 @@
package net.corda.core.internal package net.corda.core.internal
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.crypto.keys import net.corda.core.crypto.keys
import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.cordapp.CordappImpl
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor

View File

@ -524,6 +524,7 @@ fun <E> MutableSet<E>.toSynchronised(): MutableSet<E> = Collections.synchronized
/** /**
* List implementation that applies the expensive [transform] function only when the element is accessed and caches calculated values. * List implementation that applies the expensive [transform] function only when the element is accessed and caches calculated values.
* Size is very cheap as it doesn't call [transform]. * Size is very cheap as it doesn't call [transform].
* Used internally by [net.corda.core.transactions.TraversableTransaction].
*/ */
class LazyMappedList<T, U>(val originalList: List<T>, val transform: (T, Int) -> U) : AbstractList<U>() { class LazyMappedList<T, U>(val originalList: List<T>, val transform: (T, Int) -> U) : AbstractList<U>() {
private val partialResolvedList = MutableList<U?>(originalList.size) { null } private val partialResolvedList = MutableList<U?>(originalList.size) { null }
@ -532,6 +533,15 @@ class LazyMappedList<T, U>(val originalList: List<T>, val transform: (T, Int) ->
return partialResolvedList[index] return partialResolvedList[index]
?: transform(originalList[index], index).also { computed -> partialResolvedList[index] = computed } ?: transform(originalList[index], index).also { computed -> partialResolvedList[index] = computed }
} }
internal fun eager(onError: (TransactionDeserialisationException, Int) -> U?) {
for (i in 0 until size) {
try {
get(i)
} catch (ex: TransactionDeserialisationException) {
partialResolvedList[i] = onError(ex, i)
}
}
}
} }
/** /**
@ -540,6 +550,17 @@ class LazyMappedList<T, U>(val originalList: List<T>, val transform: (T, Int) ->
*/ */
fun <T, U> List<T>.lazyMapped(transform: (T, Int) -> U): List<U> = LazyMappedList(this, transform) fun <T, U> List<T>.lazyMapped(transform: (T, Int) -> U): List<U> = LazyMappedList(this, transform)
/**
* Iterate over a [LazyMappedList], forcing it to transform all of its elements immediately.
* This transformation is assumed to be "deserialisation". Does nothing for any other kind of [List].
* WARNING: Any changes made to the [LazyMappedList] contents are PERMANENT!
*/
fun <T> List<T>.eagerDeserialise(onError: (TransactionDeserialisationException, Int) -> T? = { ex, _ -> throw ex }) {
if (this is LazyMappedList<*, T>) {
eager(onError)
}
}
private const val MAX_SIZE = 100 private const val MAX_SIZE = 100
private val warnings = Collections.newSetFromMap(createSimpleCache<String, Boolean>(MAX_SIZE)).toSynchronised() private val warnings = Collections.newSetFromMap(createSimpleCache<String, Boolean>(MAX_SIZE)).toSynchronised()

View File

@ -9,7 +9,6 @@ import net.corda.core.contracts.ComponentGroupEnum.OUTPUTS_GROUP
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.* import net.corda.core.internal.*
import net.corda.core.internal.cordapp.CordappImpl.Companion.DEFAULT_CORDAPP_VERSION
import net.corda.core.node.NetworkParameters import net.corda.core.node.NetworkParameters
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.node.ServicesForResolution import net.corda.core.node.ServicesForResolution

View File

@ -4,15 +4,12 @@ package net.corda.core.utilities
import net.corda.core.DeleteForDJVM import net.corda.core.DeleteForDJVM
import net.corda.core.KeepForDJVM import net.corda.core.KeepForDJVM
import net.corda.core.internal.LazyMappedList
import net.corda.core.internal.concurrent.get import net.corda.core.internal.concurrent.get
import net.corda.core.internal.createSimpleCache
import net.corda.core.internal.uncheckedCast import net.corda.core.internal.uncheckedCast
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.time.Duration import java.time.Duration
import java.util.*
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import java.util.concurrent.Future import java.util.concurrent.Future
import kotlin.reflect.KProperty import kotlin.reflect.KProperty

View File

@ -1,11 +1,20 @@
package net.corda.core.utilities package net.corda.core.utilities
import net.corda.core.contracts.ComponentGroupEnum.*
import net.corda.core.internal.lazyMapped import net.corda.core.internal.lazyMapped
import net.corda.core.internal.TransactionDeserialisationException
import net.corda.core.internal.eagerDeserialise
import net.corda.core.serialization.MissingAttachmentsException
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.ExpectedException
import kotlin.test.assertEquals import kotlin.test.assertEquals
class LazyMappedListTest { class LazyMappedListTest {
@get:Rule
val exception: ExpectedException = ExpectedException.none()
@Test @Test
fun `LazyMappedList works`() { fun `LazyMappedList works`() {
val originalList = (1 until 10).toList() val originalList = (1 until 10).toList()
@ -33,4 +42,29 @@ class LazyMappedListTest {
assertEquals(1, callCounter) assertEquals(1, callCounter)
} }
@Test
fun testMissingAttachments() {
exception.expect(MissingAttachmentsException::class.java)
exception.expectMessage("Uncatchable!")
val lazyList = (0 until 5).toList().lazyMapped<Int, Int> { _, _ ->
throw MissingAttachmentsException(emptyList(), "Uncatchable!")
}
lazyList.eagerDeserialise { _, _ -> -999 }
}
@Test
fun testDeserialisationExceptions() {
val lazyList = (0 until 5).toList().lazyMapped<Int, Int> { _, index ->
throw TransactionDeserialisationException(
OUTPUTS_GROUP, index, IllegalStateException("Catch this!"))
}
lazyList.eagerDeserialise { _, _ -> -999 }
assertEquals(5, lazyList.size)
lazyList.forEachIndexed { idx, item ->
assertEquals(-999, item, "Item[$idx] mismatch")
}
}
} }