diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 54e2e09c99..74a5256510 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -564,7 +564,7 @@ public final class net.corda.core.contracts.ContractsDSL extends java.lang.Objec public static final java.util.List> select(java.util.Collection>, Class, java.util.Collection, java.util.Collection) ## @CordaSerializable -public interface net.corda.core.contracts.FungibleAsset extends net.corda.core.contracts.OwnableState +public interface net.corda.core.contracts.FungibleAsset extends net.corda.core.contracts.FungibleState, net.corda.core.contracts.OwnableState @NotNull public abstract net.corda.core.contracts.Amount> getAmount() @NotNull @@ -572,6 +572,11 @@ public interface net.corda.core.contracts.FungibleAsset extends net.corda.core.c @NotNull public abstract net.corda.core.contracts.FungibleAsset withNewOwnerAndAmount(net.corda.core.contracts.Amount>, net.corda.core.identity.AbstractParty) ## +@CordaSerializable +public interface net.corda.core.contracts.FungibleState extends net.corda.core.contracts.ContractState + @NotNull + public abstract net.corda.core.contracts.Amount getAmount() +## @DoNotImplement @CordaSerializable public final class net.corda.core.contracts.HashAttachmentConstraint extends java.lang.Object implements net.corda.core.contracts.AttachmentConstraint @@ -3369,7 +3374,7 @@ public interface net.corda.core.node.services.VaultService public abstract net.corda.core.messaging.DataFeed, net.corda.core.node.services.Vault$Update> trackBy(Class, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.Sort) @Suspendable @NotNull - public abstract java.util.List> tryLockFungibleStatesForSpending(java.util.UUID, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.contracts.Amount, Class) + public abstract java.util.List> tryLockFungibleStatesForSpending(java.util.UUID, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.contracts.Amount, Class) @NotNull public abstract net.corda.core.concurrent.CordaFuture> whenConsumed(net.corda.core.contracts.StateRef) ## diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 07e9cedd03..5ec2f2fe72 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -199,6 +199,8 @@ + + @@ -340,4 +342,4 @@ - \ No newline at end of file + diff --git a/buildCacheSettings.gradle b/buildCacheSettings.gradle index fcfc1513bf..b4d0175f6e 100644 --- a/buildCacheSettings.gradle +++ b/buildCacheSettings.gradle @@ -9,6 +9,7 @@ buildCache { enabled = !isCiServer } remote(HttpBuildCache) { + enabled = isCiServer url = gradleBuildCacheURL push = isCiServer } diff --git a/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt b/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt index ddcd04be56..78878461f4 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt @@ -28,12 +28,12 @@ class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException * (GBP, USD, oil, shares in company , etc.) and any additional metadata (issuer, grade, class, etc.). */ @KeepForDJVM -interface FungibleAsset : OwnableState { +interface FungibleAsset : FungibleState>, OwnableState { /** * Amount represents a positive quantity of some issued product which can be cash, tokens, assets, or generally * anything else that's quantifiable with integer quantities. See [Issued] and [Amount] for more details. */ - val amount: Amount> + override val amount: Amount> /** * There must be an ExitCommand signed by these keys to destroy the amount. While all states require their diff --git a/core/src/main/kotlin/net/corda/core/contracts/FungibleState.kt b/core/src/main/kotlin/net/corda/core/contracts/FungibleState.kt new file mode 100644 index 0000000000..0e4a241ccd --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/contracts/FungibleState.kt @@ -0,0 +1,37 @@ +package net.corda.core.contracts + +import net.corda.core.KeepForDJVM + +/** + * Interface to represent things which are fungible, this means that there is an expectation that these things can + * be split and merged. That's the only assumption made by this interface. + * + * This interface has been defined in addition to [FungibleAsset] to provide some additional flexibility which + * [FungibleAsset] lacks, in particular: + * + * - [FungibleAsset] defines an amount property of type Amount>, therefore there is an assumption that all + * fungible things are issued by a single well known party but this is not always the case. For example, + * crypto-currencies like Bitcoin are generated periodically by a pool of pseudo-anonymous miners + * and Corda can support such crypto-currencies. + * - [FungibleAsset] implements [OwnableState], as such there is an assumption that all fungible things are ownable. + * This is not always true as fungible derivative contracts exist, for example. + * + * The expectation is that this interface should be combined with the other core state interfaces such as + * [OwnableState] and others created at the application layer. + * + * @param T a type that represents the fungible thing in question. This should describe the basic type of the asset + * (GBP, USD, oil, shares in company , etc.) and any additional metadata (issuer, grade, class, etc.). An + * upper-bound is not specified for [T] to ensure flexibility. Typically, a class would be provided that implements + * [TokenizableAssetInfo]. + */ +// DOCSTART 1 +@KeepForDJVM +interface FungibleState : ContractState { + /** + * Amount represents a positive quantity of some token which can be cash, tokens, stock, agreements, or generally + * anything else that's quantifiable with integer quantities. See [Amount] for more details. + */ + val amount: Amount +} +// DOCEND 1 + diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt index 29db493d86..980589e9cb 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerificationException.kt @@ -116,13 +116,32 @@ sealed class TransactionVerificationException(val txId: SecureHash, message: Str class TransactionMissingEncumbranceException(txId: SecureHash, val missing: Int, val inOut: Direction) : TransactionVerificationException(txId, "Missing required encumbrance $missing in $inOut", null) + /** + * If two or more states refer to another state (as their encumbrance), then the bi-directionality property cannot + * be satisfied. + */ + @KeepForDJVM + class TransactionDuplicateEncumbranceException(txId: SecureHash, index: Int) + : TransactionVerificationException(txId, "The bi-directionality property of encumbered output states " + + "is not satisfied. Index $index is referenced more than once", null) + + /** + * An encumbered state should also be referenced as the encumbrance of another state in order to satisfy the + * bi-directionality property (a full cycle should be present). + */ + @KeepForDJVM + class TransactionNonMatchingEncumbranceException(txId: SecureHash, nonMatching: Collection) + : TransactionVerificationException(txId, "The bi-directionality property of encumbered output states " + + "is not satisfied. Encumbered states should also be referenced as an encumbrance of another state to form " + + "a full cycle. Offending indices $nonMatching", null) + /** Whether the inputs or outputs list contains an encumbrance issue, see [TransactionMissingEncumbranceException]. */ @CordaSerializable @KeepForDJVM enum class Direction { - /** Issue in the inputs list */ + /** Issue in the inputs list. */ INPUT, - /** Issue in the outputs list */ + /** Issue in the outputs list. */ OUTPUT } diff --git a/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt b/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt index 55946b4817..53f547076b 100644 --- a/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/AbstractStateReplacementFlow.kt @@ -62,6 +62,7 @@ abstract class AbstractStateReplacementFlow { @Throws(StateReplacementException::class) override fun call(): StateAndRef { val (stx) = assembleTx() + stx.verify(serviceHub, checkSufficientSignatures = false) val participantSessions = getParticipantSessions() progressTracker.currentStep = SIGNING diff --git a/core/src/main/kotlin/net/corda/core/flows/NotaryChangeFlow.kt b/core/src/main/kotlin/net/corda/core/flows/NotaryChangeFlow.kt index 7690f01843..18b98ae2d4 100644 --- a/core/src/main/kotlin/net/corda/core/flows/NotaryChangeFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/NotaryChangeFlow.kt @@ -46,14 +46,14 @@ class NotaryChangeFlow( return AbstractStateReplacementFlow.UpgradeTx(stx) } - /** Resolves the encumbrance state chain for the given [state] */ + /** Resolves the encumbrance state chain for the given [state]. */ private fun resolveEncumbrances(state: StateAndRef): List> { - val states = mutableListOf(state) + val states = mutableSetOf(state) while (states.last().state.encumbrance != null) { val encumbranceStateRef = StateRef(states.last().ref.txhash, states.last().state.encumbrance!!) val encumbranceState = serviceHub.toStateAndRef(encumbranceStateRef) - states.add(encumbranceState) + if (!states.add(encumbranceState)) break // Stop if there is a cycle. } - return states + return states.toList() } } diff --git a/core/src/main/kotlin/net/corda/core/internal/FlowAsyncOperation.kt b/core/src/main/kotlin/net/corda/core/internal/FlowAsyncOperation.kt index 48959c885d..fb02a4da97 100644 --- a/core/src/main/kotlin/net/corda/core/internal/FlowAsyncOperation.kt +++ b/core/src/main/kotlin/net/corda/core/internal/FlowAsyncOperation.kt @@ -12,8 +12,14 @@ import net.corda.core.serialization.CordaSerializable */ @CordaSerializable interface FlowAsyncOperation { - /** Performs the operation in a non-blocking fashion. */ - fun execute(): CordaFuture + /** + * Performs the operation in a non-blocking fashion. + * @param deduplicationId If the flow restarts from a checkpoint (due to node restart, or via a visit to the flow + * hospital following an error) the execute method might be called more than once by the Corda flow state machine. + * For each duplicate call, the deduplicationId is guaranteed to be the same allowing duplicate requests to be + * de-duplicated if necessary inside the execute method. + */ + fun execute(deduplicationId: String): CordaFuture } // DOCEND FlowAsyncOperation @@ -24,4 +30,4 @@ fun FlowLogic.executeAsync(operation: FlowAsyncOperation, may val request = FlowIORequest.ExecuteAsyncOperation(operation) return stateMachine.suspend(request, maySkipCheckpoint) } -// DOCEND executeAsync \ No newline at end of file +// DOCEND executeAsync diff --git a/core/src/main/kotlin/net/corda/core/internal/WaitForStateConsumption.kt b/core/src/main/kotlin/net/corda/core/internal/WaitForStateConsumption.kt index 6c6dcb8605..ad6d94f93f 100644 --- a/core/src/main/kotlin/net/corda/core/internal/WaitForStateConsumption.kt +++ b/core/src/main/kotlin/net/corda/core/internal/WaitForStateConsumption.kt @@ -22,7 +22,7 @@ class WaitForStateConsumption(val stateRefs: Set, val services: Servic val logger = contextLogger() } - override fun execute(): CordaFuture { + override fun execute(deduplicationId: String): CordaFuture { val futures = stateRefs.map { services.vaultService.whenConsumed(it).toCompletableFuture() } val completedFutures = futures.filter { it.isDone } @@ -40,4 +40,4 @@ class WaitForStateConsumption(val stateRefs: Set, val services: Servic return CompletableFuture.allOf(*futures.toTypedArray()).thenApply { Unit }.asCordaFuture() } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/net/corda/core/internal/notary/AsyncCFTNotaryService.kt b/core/src/main/kotlin/net/corda/core/internal/notary/AsyncCFTNotaryService.kt index d8d111d2c2..ea925d1def 100644 --- a/core/src/main/kotlin/net/corda/core/internal/notary/AsyncCFTNotaryService.kt +++ b/core/src/main/kotlin/net/corda/core/internal/notary/AsyncCFTNotaryService.kt @@ -69,7 +69,7 @@ abstract class AsyncCFTNotaryService : TrustedAuthorityNotaryService() { val timeWindow: TimeWindow?, val references: List ): FlowAsyncOperation { - override fun execute(): CordaFuture { + override fun execute(deduplicationId: String): CordaFuture { return service.commitAsync(inputs, txId, caller, requestSignature, timeWindow, references) } } diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index 43b71bf966..a9087e12af 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -334,24 +334,26 @@ interface VaultService { /** * Helper function to determine spendable states and soft locking them. - * Currently performance will be worse than for the hand optimised version in `Cash.unconsumedCashStatesForSpending`. - * However, this is fully generic and can operate with custom [FungibleAsset] states. + * Currently performance will be worse than for the hand optimised version in + * [Cash.unconsumedCashStatesForSpending]. However, this is fully generic and can operate with custom [FungibleState] + * and [FungibleAsset] states. * @param lockId The [FlowLogic.runId]'s [UUID] of the current flow used to soft lock the states. * @param eligibleStatesQuery A custom query object that selects down to the appropriate subset of all states of the * [contractStateType]. e.g. by selecting on account, issuer, etc. The query is internally augmented with the * [StateStatus.UNCONSUMED], soft lock and contract type requirements. - * @param amount The required amount of the asset, but with the issuer stripped off. - * It is assumed that compatible issuer states will be filtered out by the [eligibleStatesQuery]. + * @param amount The required amount of the asset. It is assumed that compatible issuer states will be filtered out + * by the [eligibleStatesQuery]. This method accepts both Amount> and Amount<*>. Amount> is + * automatically unwrapped to Amount<*>. * @param contractStateType class type of the result set. * @return Returns a locked subset of the [eligibleStatesQuery] sufficient to satisfy the requested amount, * or else an empty list and no change in the stored lock states when their are insufficient resources available. */ @Suspendable @Throws(StatesNotAvailableException::class) - fun , U : Any> tryLockFungibleStatesForSpending(lockId: UUID, - eligibleStatesQuery: QueryCriteria, - amount: Amount, - contractStateType: Class): List> + fun > tryLockFungibleStatesForSpending(lockId: UUID, + eligibleStatesQuery: QueryCriteria, + amount: Amount<*>, + contractStateType: Class): List> // DOCSTART VaultQueryAPI /** diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index 2ee555dee1..553320c7fa 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -168,6 +168,22 @@ sealed class QueryCriteria : GenericQueryCriteria? = null, + val quantity: ColumnPredicate? = null, + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + override val contractStateTypes: Set>? = null, + override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL + ) : CommonQueryCriteria() { + override fun visit(parser: IQueryCriteriaParser): Collection { + super.visit(parser) + return parser.parseCriteria(this) + } + } + /** * FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleStates] */ diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 12e444e063..51c776734f 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -199,30 +199,79 @@ data class LedgerTransaction @JvmOverloads constructor( private fun checkEncumbrancesValid() { // Validate that all encumbrances exist within the set of input states. - val encumberedInputs = inputs.filter { it.state.encumbrance != null } - encumberedInputs.forEach { (state, ref) -> - val encumbranceStateExists = inputs.any { - it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance - } - if (!encumbranceStateExists) { + inputs.filter { it.state.encumbrance != null } + .forEach { (state, ref) -> checkInputEncumbranceStateExists(state, ref) } + + // Check that in the outputs, + // a) an encumbered state does not refer to itself as the encumbrance + // b) the number of outputs can contain the encumbrance + // c) the bi-directionality (full cycle) property is satisfied. + val statesAndEncumbrance = outputs.withIndex().filter { it.value.encumbrance != null }.map { Pair(it.index, it.value.encumbrance!!) } + if (!statesAndEncumbrance.isEmpty()) { + checkOutputEncumbrances(statesAndEncumbrance) + } + } + + private fun checkInputEncumbranceStateExists(state: TransactionState, ref: StateRef) { + val encumbranceStateExists = inputs.any { + it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance + } + if (!encumbranceStateExists) { + throw TransactionVerificationException.TransactionMissingEncumbranceException( + id, + state.encumbrance!!, + TransactionVerificationException.Direction.INPUT + ) + } + } + + // Using basic graph theory, a full cycle of encumbered (co-dependent) states should exist to achieve bi-directional + // encumbrances. This property is important to ensure that no states involved in an encumbrance-relationship + // can be spent on their own. Briefly, if any of the states is having more than one encumbrance references by + // other states, a full cycle detection will fail. As a result, all of the encumbered states must be present + // as "from" and "to" only once (or zero times if no encumbrance takes place). For instance, + // a -> b + // c -> b and a -> b + // b -> a b -> c + // do not satisfy the bi-directionality (full cycle) property. + // + // In the first example "b" appears twice in encumbrance ("to") list and "c" exists in the encumbered ("from") list only. + // Due the above, one could consume "a" and "b" in the same transaction and then, because "b" is already consumed, "c" cannot be spent. + // + // Similarly, the second example does not form a full cycle because "a" and "c" exist in one of the lists only. + // As a result, one can consume "b" and "c" in the same transactions, which will make "a" impossible to be spent. + // + // On other hand the following are valid constructions: + // a -> b a -> c + // b -> c and c -> b + // c -> a b -> a + // and form a full cycle, meaning that the bi-directionality property is satisfied. + private fun checkOutputEncumbrances(statesAndEncumbrance: List>) { + // [Set] of "from" (encumbered states). + val encumberedSet = mutableSetOf() + // [Set] of "to" (encumbrance states). + val encumbranceSet = mutableSetOf() + // Update both [Set]s. + statesAndEncumbrance.forEach { (statePosition, encumbrance) -> + // Check it does not refer to itself. + if (statePosition == encumbrance || encumbrance >= outputs.size) { throw TransactionVerificationException.TransactionMissingEncumbranceException( id, - state.encumbrance!!, - TransactionVerificationException.Direction.INPUT - ) + encumbrance, + TransactionVerificationException.Direction.OUTPUT) + } else { + encumberedSet.add(statePosition) // Guaranteed to have unique elements. + if (!encumbranceSet.add(encumbrance)) { + throw TransactionVerificationException.TransactionDuplicateEncumbranceException(id, encumbrance) + } } } - - // Check that, in the outputs, an encumbered state does not refer to itself as the encumbrance, - // and that the number of outputs can contain the encumbrance. - for ((i, output) in outputs.withIndex()) { - val encumbranceIndex = output.encumbrance ?: continue - if (encumbranceIndex == i || encumbranceIndex >= outputs.size) { - throw TransactionVerificationException.TransactionMissingEncumbranceException( - id, - encumbranceIndex, - TransactionVerificationException.Direction.OUTPUT) - } + // At this stage we have ensured that "from" and "to" [Set]s are equal in size, but we should check their + // elements do indeed match. If they don't match, we return their symmetric difference (disjunctive union). + val symmetricDifference = (encumberedSet union encumbranceSet).subtract(encumberedSet intersect encumbranceSet) + if (symmetricDifference.isNotEmpty()) { + // At least one encumbered state is not in the [encumbranceSet] and vice versa. + throw TransactionVerificationException.TransactionNonMatchingEncumbranceException(id, symmetricDifference) } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt index 2603b6ca3c..55f329540d 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/NotaryChangeTransactions.kt @@ -102,13 +102,21 @@ data class NotaryChangeLedgerTransaction( override val references: List> = emptyList() - /** We compute the outputs on demand by applying the notary field modification to the inputs */ + /** We compute the outputs on demand by applying the notary field modification to the inputs. */ override val outputs: List> - get() = inputs.mapIndexed { pos, (state) -> + get() = computeOutputs() + + private fun computeOutputs(): List> { + val inputPositionIndex: Map = inputs.mapIndexed { index, stateAndRef -> stateAndRef.ref to index }.toMap() + return inputs.map { (state, ref) -> if (state.encumbrance != null) { - state.copy(notary = newNotary, encumbrance = pos + 1) + val encumbranceStateRef = StateRef(ref.txhash, state.encumbrance) + val encumbrancePosition = inputPositionIndex[encumbranceStateRef] + ?: throw IllegalStateException("Unable to generate output states – transaction not constructed correctly.") + state.copy(notary = newNotary, encumbrance = encumbrancePosition) } else state.copy(notary = newNotary) } + } override val requiredSigningKeys: Set get() = inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet() + notary.owningKey @@ -118,18 +126,16 @@ data class NotaryChangeLedgerTransaction( } /** - * Check that encumbrances have been included in the inputs. The [NotaryChangeFlow] guarantees that an encumbrance - * will follow its encumbered state in the inputs. + * Check that encumbrances have been included in the inputs. */ private fun checkEncumbrances() { - inputs.forEachIndexed { i, (state, ref) -> - state.encumbrance?.let { - val nextIndex = i + 1 - fun nextStateIsEncumbrance() = (inputs[nextIndex].ref.txhash == ref.txhash) && (inputs[nextIndex].ref.index == it) - if (nextIndex >= inputs.size || !nextStateIsEncumbrance()) { + val encumberedStates = inputs.asSequence().filter { it.state.encumbrance != null }.associateBy { it.ref } + if (encumberedStates.isNotEmpty()) { + inputs.forEach { (state, ref) -> + if (StateRef(ref.txhash, state.encumbrance!!) !in encumberedStates) { throw TransactionVerificationException.TransactionMissingEncumbranceException( id, - it, + state.encumbrance, TransactionVerificationException.Direction.INPUT) } } diff --git a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt index 240edbfd1e..97b9248be2 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt @@ -134,4 +134,3 @@ fun Future.getOrThrow(timeout: Duration? = null): V = try { } catch (e: ExecutionException) { throw e.cause!! } - diff --git a/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt b/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt index 468f9cee95..6cbd8e3483 100644 --- a/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt +++ b/core/src/test/kotlin/net/corda/core/transactions/TransactionEncumbranceTests.kt @@ -4,6 +4,7 @@ import com.nhaarman.mockito_kotlin.doReturn import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.Contract import net.corda.core.contracts.ContractState +import net.corda.core.contracts.TransactionVerificationException import net.corda.core.contracts.requireThat import net.corda.core.identity.AbstractParty import net.corda.core.identity.CordaX500Name @@ -21,33 +22,43 @@ import org.junit.Rule import org.junit.Test import java.time.Instant import java.time.temporal.ChronoUnit +import kotlin.test.assertFailsWith const val TEST_TIMELOCK_ID = "net.corda.core.transactions.TransactionEncumbranceTests\$DummyTimeLock" class TransactionEncumbranceTests { + @Rule + @JvmField + val testSerialization = SerializationEnvironmentRule() + private companion object { val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) val MINI_CORP = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")).party val MEGA_CORP get() = megaCorp.party val MEGA_CORP_PUBKEY get() = megaCorp.publicKey + + val defaultIssuer = MEGA_CORP.ref(1) + + val state = Cash.State( + amount = 1000.DOLLARS `issued by` defaultIssuer, + owner = MEGA_CORP + ) + + val stateWithNewOwner = state.copy(owner = MINI_CORP) + val extraCashState = state.copy(amount = state.amount * 3) + + val FOUR_PM: Instant = Instant.parse("2015-04-17T16:00:00.00Z") + val FIVE_PM: Instant = FOUR_PM.plus(1, ChronoUnit.HOURS) + val timeLock = DummyTimeLock.State(FIVE_PM) + + + val ledgerServices = MockServices(listOf("net.corda.core.transactions", "net.corda.finance.contracts.asset"), MEGA_CORP.name, + rigorousMock().also { + doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) + }) } - @Rule - @JvmField - val testSerialization = SerializationEnvironmentRule() - val defaultIssuer = MEGA_CORP.ref(1) - - val state = Cash.State( - amount = 1000.DOLLARS `issued by` defaultIssuer, - owner = MEGA_CORP - ) - val stateWithNewOwner = state.copy(owner = MINI_CORP) - - val FOUR_PM: Instant = Instant.parse("2015-04-17T16:00:00.00Z") - val FIVE_PM: Instant = FOUR_PM.plus(1, ChronoUnit.HOURS) - val timeLock = DummyTimeLock.State(FIVE_PM) - class DummyTimeLock : Contract { override fun verify(tx: LedgerTransaction) { val timeLockInput = tx.inputsOfType().singleOrNull() ?: return @@ -65,23 +76,136 @@ class TransactionEncumbranceTests { } } - private val ledgerServices = MockServices(listOf("net.corda.core.transactions", "net.corda.finance.contracts.asset"), MEGA_CORP.name, - rigorousMock().also { - doReturn(MEGA_CORP).whenever(it).partyFromKey(MEGA_CORP_PUBKEY) - }) - @Test - fun `state can be encumbered`() { + fun `states can be bi-directionally encumbered`() { + // Basic encumbrance example for encumbrance index links 0 -> 1 and 1 -> 0 ledgerServices.ledger(DUMMY_NOTARY) { transaction { attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) input(Cash.PROGRAM_ID, state) - output(Cash.PROGRAM_ID, encumbrance = 1, contractState = stateWithNewOwner) - output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock) + output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", encumbrance = 1, contractState = stateWithNewOwner) + output(TEST_TIMELOCK_ID, "5pm time-lock", 0, timeLock) command(MEGA_CORP.owningKey, Cash.Commands.Move()) verifies() } } + + // Full cycle example with 4 elements 0 -> 1, 1 -> 2, 2 -> 3 and 3 -> 0 + // All 3 Cash states and the TimeLock are linked and should be consumed in the same transaction. + // Note that all of the Cash states are encumbered both together and with time lock. + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) + input(Cash.PROGRAM_ID, extraCashState) + output(Cash.PROGRAM_ID, "state encumbered by state 1", encumbrance = 1, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by state 2", encumbrance = 2, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by state 3", encumbrance = 3, contractState = stateWithNewOwner) + output(TEST_TIMELOCK_ID, "5pm time-lock", 0, timeLock) + command(MEGA_CORP.owningKey, Cash.Commands.Move()) + verifies() + } + } + + // A transaction that includes multiple independent encumbrance chains. + // Each Cash state is encumbered with its own TimeLock. + // Note that all of the Cash states are encumbered both together and with time lock. + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) + input(Cash.PROGRAM_ID, extraCashState) + output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock A", encumbrance = 3, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock B", encumbrance = 4, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock C", encumbrance = 5, contractState = stateWithNewOwner) + output(TEST_TIMELOCK_ID, "5pm time-lock A", 0, timeLock) + output(TEST_TIMELOCK_ID, "5pm time-lock B", 1, timeLock) + output(TEST_TIMELOCK_ID, "5pm time-lock C", 2, timeLock) + command(MEGA_CORP.owningKey, Cash.Commands.Move()) + verifies() + } + } + + // Full cycle example with 4 elements (different combination) 0 -> 3, 1 -> 2, 2 -> 0 and 3 -> 1 + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) + input(Cash.PROGRAM_ID, extraCashState) + output(Cash.PROGRAM_ID, "state encumbered by state 3", encumbrance = 3, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by state 2", encumbrance = 2, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by state 0", encumbrance = 0, contractState = stateWithNewOwner) + output(TEST_TIMELOCK_ID, "5pm time-lock", 1, timeLock) + command(MEGA_CORP.owningKey, Cash.Commands.Move()) + verifies() + } + } + } + + @Test + fun `non bi-directional encumbrance will fail`() { + // Single encumbrance with no back link. + assertFailsWith { + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) + input(Cash.PROGRAM_ID, state) + output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", encumbrance = 1, contractState = stateWithNewOwner) + output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock) + command(MEGA_CORP.owningKey, Cash.Commands.Move()) + verifies() + } + } + } + + // Full cycle fails due to duplicate encumbrance reference. + // 0 -> 1, 1 -> 3, 2 -> 3 (thus 3 is referenced two times). + assertFailsWith { + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) + input(Cash.PROGRAM_ID, state) + output(Cash.PROGRAM_ID, "state encumbered by state 1", encumbrance = 1, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by state 3", encumbrance = 3, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by state 3 again", encumbrance = 3, contractState = stateWithNewOwner) + output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock) + command(MEGA_CORP.owningKey, Cash.Commands.Move()) + verifies() + } + } + } + + // No Full cycle due to non-matching encumbered-encumbrance elements. + // 0 -> 1, 1 -> 3, 2 -> 0 (thus offending indices [2, 3], because 2 is not referenced and 3 is not encumbered). + assertFailsWith { + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) + input(Cash.PROGRAM_ID, state) + output(Cash.PROGRAM_ID, "state encumbered by state 1", encumbrance = 1, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by state 3", encumbrance = 3, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by state 0", encumbrance = 0, contractState = stateWithNewOwner) + output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock) + command(MEGA_CORP.owningKey, Cash.Commands.Move()) + verifies() + } + } + } + + // No Full cycle in one of the encumbrance chains due to non-matching encumbered-encumbrance elements. + // 0 -> 2, 2 -> 0 is valid. On the other hand, there is 1 -> 3 only and 3 -> 1 does not exist. + // (thus offending indices [1, 3], because 1 is not referenced and 3 is not encumbered). + assertFailsWith { + ledgerServices.ledger(DUMMY_NOTARY) { + transaction { + attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) + input(Cash.PROGRAM_ID, state) + output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock A", encumbrance = 2, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock B", encumbrance = 3, contractState = stateWithNewOwner) + output(TEST_TIMELOCK_ID, "5pm time-lock A", 0, timeLock) + output(TEST_TIMELOCK_ID, "5pm time-lock B", timeLock) + command(MEGA_CORP.owningKey, Cash.Commands.Move()) + verifies() + } + } + } } @Test @@ -132,7 +256,7 @@ class TransactionEncumbranceTests { unverifiedTransaction { attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) output(Cash.PROGRAM_ID, "state encumbered by 5pm time-lock", encumbrance = 1, contractState = state) - output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock) + output(TEST_TIMELOCK_ID, "5pm time-lock",0, timeLock) } transaction { attachments(Cash.PROGRAM_ID) @@ -151,7 +275,7 @@ class TransactionEncumbranceTests { transaction { attachments(Cash.PROGRAM_ID) input(Cash.PROGRAM_ID, state) - output(Cash.PROGRAM_ID, encumbrance = 0, contractState = stateWithNewOwner) + output(Cash.PROGRAM_ID, "state encumbered by itself", encumbrance = 0, contractState = stateWithNewOwner) command(MEGA_CORP.owningKey, Cash.Commands.Move()) this `fails with` "Missing required encumbrance 0 in OUTPUT" } @@ -164,7 +288,7 @@ class TransactionEncumbranceTests { transaction { attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) input(Cash.PROGRAM_ID, state) - output(TEST_TIMELOCK_ID, encumbrance = 2, contractState = stateWithNewOwner) + output(TEST_TIMELOCK_ID, "state encumbered by state 2 which does not exist", encumbrance = 2, contractState = stateWithNewOwner) output(TEST_TIMELOCK_ID, timeLock) command(MEGA_CORP.owningKey, Cash.Commands.Move()) this `fails with` "Missing required encumbrance 2 in OUTPUT" @@ -178,7 +302,7 @@ class TransactionEncumbranceTests { unverifiedTransaction { attachments(Cash.PROGRAM_ID, TEST_TIMELOCK_ID) output(Cash.PROGRAM_ID, "state encumbered by some other state", encumbrance = 1, contractState = state) - output(Cash.PROGRAM_ID, "some other state", state) + output(Cash.PROGRAM_ID, "some other state", encumbrance = 0, contractState = state) output(TEST_TIMELOCK_ID, "5pm time-lock", timeLock) } transaction { diff --git a/djvm/build.gradle b/djvm/build.gradle index d40a4d5523..1b33bdd3ae 100644 --- a/djvm/build.gradle +++ b/djvm/build.gradle @@ -33,7 +33,6 @@ dependencies { // ASM: byte code manipulation library compile "org.ow2.asm:asm:$asm_version" - compile "org.ow2.asm:asm-tree:$asm_version" compile "org.ow2.asm:asm-commons:$asm_version" // ClassGraph: classpath scanning @@ -62,6 +61,7 @@ shadowJar { exclude 'sandbox/java/lang/Comparable.class' exclude 'sandbox/java/lang/Enum.class' exclude 'sandbox/java/lang/Iterable.class' + exclude 'sandbox/java/lang/StackTraceElement.class' exclude 'sandbox/java/lang/StringBuffer.class' exclude 'sandbox/java/lang/StringBuilder.class' exclude 'sandbox/java/nio/**' diff --git a/djvm/src/main/java/sandbox/java/lang/Appendable.java b/djvm/src/main/java/sandbox/java/lang/Appendable.java index 168607c511..c95eaf6e53 100644 --- a/djvm/src/main/java/sandbox/java/lang/Appendable.java +++ b/djvm/src/main/java/sandbox/java/lang/Appendable.java @@ -3,10 +3,10 @@ package sandbox.java.lang; import java.io.IOException; /** - * This is a dummy class that implements just enough of [java.lang.Appendable] - * to keep [sandbox.java.lang.StringBuilder], [sandbox.java.lang.StringBuffer] - * and [sandbox.java.lang.String] honest. - * Note that it does not extend [java.lang.Appendable]. + * This is a dummy class that implements just enough of {@link java.lang.Appendable} + * to keep {@link sandbox.java.lang.StringBuilder}, {@link sandbox.java.lang.StringBuffer} + * and {@link sandbox.java.lang.String} honest. + * Note that it does not extend {@link java.lang.Appendable}. */ public interface Appendable { diff --git a/djvm/src/main/java/sandbox/java/lang/CharSequence.java b/djvm/src/main/java/sandbox/java/lang/CharSequence.java index 1847103093..10b024d027 100644 --- a/djvm/src/main/java/sandbox/java/lang/CharSequence.java +++ b/djvm/src/main/java/sandbox/java/lang/CharSequence.java @@ -3,8 +3,8 @@ package sandbox.java.lang; import org.jetbrains.annotations.NotNull; /** - * This is a dummy class that implements just enough of [java.lang.CharSequence] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.lang.CharSequence} + * to allow us to compile {@link sandbox.java.lang.String}. */ public interface CharSequence extends java.lang.CharSequence { diff --git a/djvm/src/main/java/sandbox/java/lang/Comparable.java b/djvm/src/main/java/sandbox/java/lang/Comparable.java index 686539c1b4..59b3278a1b 100644 --- a/djvm/src/main/java/sandbox/java/lang/Comparable.java +++ b/djvm/src/main/java/sandbox/java/lang/Comparable.java @@ -1,8 +1,8 @@ package sandbox.java.lang; /** - * This is a dummy class that implements just enough of [java.lang.Comparable] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.lang.Comparable} + * to allow us to compile {@link sandbox.java.lang.String}. */ public interface Comparable extends java.lang.Comparable { } diff --git a/djvm/src/main/java/sandbox/java/lang/DJVMThrowableWrapper.java b/djvm/src/main/java/sandbox/java/lang/DJVMThrowableWrapper.java new file mode 100644 index 0000000000..60de2e117d --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/DJVMThrowableWrapper.java @@ -0,0 +1,37 @@ +package sandbox.java.lang; + +import org.jetbrains.annotations.NotNull; + +/** + * Pinned exceptions inherit from {@link java.lang.Throwable}, but we + * still need to be able to pass them through the sandbox's + * exception handlers. In which case we will wrap them inside + * one of these. + * + * Exceptions wrapped inside one of these cannot be caught. + * + * Also used for passing exceptions through finally blocks without + * any expensive unwrapping to {@link sandbox.java.lang.Throwable} + * based types. + */ +final class DJVMThrowableWrapper extends Throwable { + private final java.lang.Throwable throwable; + + DJVMThrowableWrapper(java.lang.Throwable t) { + throwable = t; + } + + /** + * Prevent this wrapper from creating its own stack trace. + */ + @Override + public final Throwable fillInStackTrace() { + return this; + } + + @Override + @NotNull + final java.lang.Throwable fromDJVM() { + return throwable; + } +} diff --git a/djvm/src/main/java/sandbox/java/lang/Enum.java b/djvm/src/main/java/sandbox/java/lang/Enum.java index ffcdd8c916..d3a4bf352e 100644 --- a/djvm/src/main/java/sandbox/java/lang/Enum.java +++ b/djvm/src/main/java/sandbox/java/lang/Enum.java @@ -1,11 +1,13 @@ package sandbox.java.lang; +import org.jetbrains.annotations.NotNull; + import java.io.Serializable; /** * This is a dummy class. We will load the actual Enum class at run-time. */ -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "WeakerAccess"}) public abstract class Enum> extends Object implements Comparable, Serializable { private final String name; @@ -24,4 +26,10 @@ public abstract class Enum> extends Object implements Comparab return ordinal; } + @Override + @NotNull + final java.lang.Enum fromDJVM() { + throw new UnsupportedOperationException("Dummy implementation"); + } + } diff --git a/djvm/src/main/java/sandbox/java/lang/Iterable.java b/djvm/src/main/java/sandbox/java/lang/Iterable.java index 6032fd97db..01f8108ac0 100644 --- a/djvm/src/main/java/sandbox/java/lang/Iterable.java +++ b/djvm/src/main/java/sandbox/java/lang/Iterable.java @@ -5,8 +5,8 @@ import org.jetbrains.annotations.NotNull; import java.util.Iterator; /** - * This is a dummy class that implements just enough of [java.lang.Iterable] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.lang.Iterable} + * to allow us to compile {@link sandbox.java.lang.String}. */ public interface Iterable extends java.lang.Iterable { @Override diff --git a/djvm/src/main/java/sandbox/java/lang/Object.java b/djvm/src/main/java/sandbox/java/lang/Object.java index 4208a52a53..62ac16d4dd 100644 --- a/djvm/src/main/java/sandbox/java/lang/Object.java +++ b/djvm/src/main/java/sandbox/java/lang/Object.java @@ -54,8 +54,7 @@ public class Object { private static Class fromDJVM(Class type) { try { - java.lang.String name = type.getName(); - return Class.forName(name.startsWith("sandbox.") ? name.substring(8) : name); + return DJVM.fromDJVMType(type); } catch (ClassNotFoundException e) { throw new RuleViolationError(e.getMessage()); } diff --git a/djvm/src/main/java/sandbox/java/lang/StackTraceElement.java b/djvm/src/main/java/sandbox/java/lang/StackTraceElement.java new file mode 100644 index 0000000000..7b8173134a --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/StackTraceElement.java @@ -0,0 +1,46 @@ +package sandbox.java.lang; + +import org.jetbrains.annotations.NotNull; + +/** + * This is a dummy class. We will load the genuine class at runtime. + */ +public final class StackTraceElement extends Object implements java.io.Serializable { + + private final String className; + private final String methodName; + private final String fileName; + private final int lineNumber; + + public StackTraceElement(String className, String methodName, String fileName, int lineNumber) { + this.className = className; + this.methodName = methodName; + this.fileName = fileName; + this.lineNumber = lineNumber; + } + + public String getClassName() { + return className; + } + + public String getMethodName() { + return methodName; + } + + public String getFileName() { + return fileName; + } + + public int getLineNumber() { + return lineNumber; + } + + @Override + @NotNull + public String toDJVMString() { + return String.toDJVM( + className.toString() + ':' + methodName.toString() + + (fileName != null ? '(' + fileName.toString() + ':' + lineNumber + ')' : "") + ); + } +} diff --git a/djvm/src/main/java/sandbox/java/lang/String.java b/djvm/src/main/java/sandbox/java/lang/String.java index 4cce494d30..476669bfe9 100644 --- a/djvm/src/main/java/sandbox/java/lang/String.java +++ b/djvm/src/main/java/sandbox/java/lang/String.java @@ -7,6 +7,8 @@ import sandbox.java.util.Locale; import java.io.Serializable; import java.io.UnsupportedEncodingException; +import java.lang.reflect.Constructor; +import java.util.Map; @SuppressWarnings("unused") public final class String extends Object implements Comparable, CharSequence, Serializable { @@ -22,6 +24,18 @@ public final class String extends Object implements Comparable, CharSequ private static final String TRUE = new String("true"); private static final String FALSE = new String("false"); + private static final Map INTERNAL = new java.util.HashMap<>(); + private static final Constructor SHARED; + + static { + try { + SHARED = java.lang.String.class.getDeclaredConstructor(char[].class, java.lang.Boolean.TYPE); + SHARED.setAccessible(true); + } catch (NoSuchMethodException e) { + throw new NoSuchMethodError(e.getMessage()); + } + } + private final java.lang.String value; public String() { @@ -88,6 +102,17 @@ public final class String extends Object implements Comparable, CharSequ this.value = builder.toString(); } + String(char[] value, boolean share) { + java.lang.String newValue; + try { + // This is (presumably) an optimisation for memory usage. + newValue = (java.lang.String) SHARED.newInstance(value, share); + } catch (Exception e) { + newValue = new java.lang.String(value); + } + this.value = newValue; + } + @Override public char charAt(int index) { return value.charAt(index); @@ -310,6 +335,8 @@ public final class String extends Object implements Comparable, CharSequ return toDJVM(value.trim()); } + public String intern() { return INTERNAL.computeIfAbsent(value, s -> this); } + public char[] toCharArray() { return value.toCharArray(); } diff --git a/djvm/src/main/java/sandbox/java/lang/StringBuffer.java b/djvm/src/main/java/sandbox/java/lang/StringBuffer.java index e9cbcad328..4d8fea7e1d 100644 --- a/djvm/src/main/java/sandbox/java/lang/StringBuffer.java +++ b/djvm/src/main/java/sandbox/java/lang/StringBuffer.java @@ -3,8 +3,8 @@ package sandbox.java.lang; import java.io.Serializable; /** - * This is a dummy class that implements just enough of [java.lang.StringBuffer] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.lang.StringBuffer} + * to allow us to compile {@link sandbox.java.lang.String}. */ public abstract class StringBuffer extends Object implements CharSequence, Appendable, Serializable { diff --git a/djvm/src/main/java/sandbox/java/lang/StringBuilder.java b/djvm/src/main/java/sandbox/java/lang/StringBuilder.java index ed80b2e508..a90fef7dde 100644 --- a/djvm/src/main/java/sandbox/java/lang/StringBuilder.java +++ b/djvm/src/main/java/sandbox/java/lang/StringBuilder.java @@ -3,8 +3,8 @@ package sandbox.java.lang; import java.io.Serializable; /** - * This is a dummy class that implements just enough of [java.lang.StringBuilder] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.lang.StringBuilder} + * to allow us to compile {@link sandbox.java.lang.String}. */ public abstract class StringBuilder extends Object implements Appendable, CharSequence, Serializable { diff --git a/djvm/src/main/java/sandbox/java/lang/Throwable.java b/djvm/src/main/java/sandbox/java/lang/Throwable.java new file mode 100644 index 0000000000..df19e2fc6e --- /dev/null +++ b/djvm/src/main/java/sandbox/java/lang/Throwable.java @@ -0,0 +1,137 @@ +package sandbox.java.lang; + +import org.jetbrains.annotations.NotNull; +import sandbox.TaskTypes; + +import java.io.Serializable; + +@SuppressWarnings({"unused", "WeakerAccess"}) +public class Throwable extends Object implements Serializable { + private static final StackTraceElement[] NO_STACK_TRACE = new StackTraceElement[0]; + + private String message; + private Throwable cause; + private StackTraceElement[] stackTrace; + + public Throwable() { + this.cause = this; + fillInStackTrace(); + } + + public Throwable(String message) { + this(); + this.message = message; + } + + public Throwable(Throwable cause) { + this.cause = cause; + this.message = (cause == null) ? null : cause.toDJVMString(); + fillInStackTrace(); + } + + public Throwable(String message, Throwable cause) { + this.message = message; + this.cause = cause; + fillInStackTrace(); + } + + protected Throwable(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + if (writableStackTrace) { + fillInStackTrace(); + } else { + stackTrace = NO_STACK_TRACE; + } + this.message = message; + this.cause = cause; + } + + public String getMessage() { + return message; + } + + public String getLocalizedMessage() { + return getMessage(); + } + + public Throwable getCause() { + return (cause == this) ? null : cause; + } + + public Throwable initCause(Throwable cause) { + if (this.cause != this) { + throw new java.lang.IllegalStateException( + "Can't overwrite cause with " + java.util.Objects.toString(cause, "a null"), fromDJVM()); + } + if (cause == this) { + throw new java.lang.IllegalArgumentException("Self-causation not permitted", fromDJVM()); + } + this.cause = cause; + return this; + } + + @Override + @NotNull + public String toDJVMString() { + java.lang.String s = getClass().getName(); + String localized = getLocalizedMessage(); + return String.valueOf((localized != null) ? (s + ": " + localized.toString()) : s); + } + + public StackTraceElement[] getStackTrace() { + return (stackTrace == NO_STACK_TRACE) ? stackTrace : stackTrace.clone(); + } + + public void setStackTrace(StackTraceElement[] stackTrace) { + StackTraceElement[] traceCopy = stackTrace.clone(); + + for (int i = 0; i < traceCopy.length; ++i) { + if (traceCopy[i] == null) { + throw new java.lang.NullPointerException("stackTrace[" + i + ']'); + } + } + + this.stackTrace = traceCopy; + } + + @SuppressWarnings({"ThrowableNotThrown", "UnusedReturnValue"}) + public Throwable fillInStackTrace() { + if (stackTrace == null) { + /* + * We have been invoked from within this exception's constructor. + * Work our way up the stack trace until we find this constructor, + * and then find out who actually invoked it. This is where our + * sandboxed stack trace will start from. + * + * Our stack trace will end at the point where we entered the sandbox. + */ + final java.lang.StackTraceElement[] elements = new java.lang.Throwable().getStackTrace(); + final java.lang.String exceptionName = getClass().getName(); + int startIdx = 1; + while (startIdx < elements.length && !isConstructorFor(elements[startIdx], exceptionName)) { + ++startIdx; + } + while (startIdx < elements.length && isConstructorFor(elements[startIdx], exceptionName)) { + ++startIdx; + } + + int endIdx = startIdx; + while (endIdx < elements.length && !TaskTypes.isEntryPoint(elements[endIdx])) { + ++endIdx; + } + stackTrace = (startIdx == elements.length) ? NO_STACK_TRACE : DJVM.copyToDJVM(elements, startIdx, endIdx); + } + return this; + } + + private static boolean isConstructorFor(java.lang.StackTraceElement elt, java.lang.String className) { + return elt.getClassName().equals(className) && elt.getMethodName().equals(""); + } + + public void printStackTrace() {} + + @Override + @NotNull + java.lang.Throwable fromDJVM() { + return DJVM.fromDJVM(this); + } +} diff --git a/djvm/src/main/java/sandbox/java/nio/charset/Charset.java b/djvm/src/main/java/sandbox/java/nio/charset/Charset.java index 371a21404a..453006bb7f 100644 --- a/djvm/src/main/java/sandbox/java/nio/charset/Charset.java +++ b/djvm/src/main/java/sandbox/java/nio/charset/Charset.java @@ -1,8 +1,8 @@ package sandbox.java.nio.charset; /** - * This is a dummy class that implements just enough of [java.nio.charset.Charset] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.nio.charset.Charset} + * to allow us to compile {@link sandbox.java.lang.String}. */ @SuppressWarnings("unused") public abstract class Charset extends sandbox.java.lang.Object { diff --git a/djvm/src/main/java/sandbox/java/util/Comparator.java b/djvm/src/main/java/sandbox/java/util/Comparator.java index 20679dee59..f6363d9c34 100644 --- a/djvm/src/main/java/sandbox/java/util/Comparator.java +++ b/djvm/src/main/java/sandbox/java/util/Comparator.java @@ -1,8 +1,8 @@ package sandbox.java.util; /** - * This is a dummy class that implements just enough of [java.util.Comparator] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.util.Comparator} + * to allow us to compile {@link sandbox.java.lang.String}. */ @FunctionalInterface public interface Comparator extends java.util.Comparator { diff --git a/djvm/src/main/java/sandbox/java/util/Locale.java b/djvm/src/main/java/sandbox/java/util/Locale.java index 3ceaea9382..ed06b79058 100644 --- a/djvm/src/main/java/sandbox/java/util/Locale.java +++ b/djvm/src/main/java/sandbox/java/util/Locale.java @@ -1,8 +1,8 @@ package sandbox.java.util; /** - * This is a dummy class that implements just enough of [java.util.Locale] - * to allow us to compile [sandbox.java.lang.String]. + * This is a dummy class that implements just enough of {@link java.util.Locale} + * to allow us to compile {@link sandbox.java.lang.String}. */ public abstract class Locale extends sandbox.java.lang.Object { public abstract sandbox.java.lang.String toLanguageTag(); diff --git a/djvm/src/main/java/sandbox/java/util/function/Function.java b/djvm/src/main/java/sandbox/java/util/function/Function.java index 5cd806a01e..ce26393f67 100644 --- a/djvm/src/main/java/sandbox/java/util/function/Function.java +++ b/djvm/src/main/java/sandbox/java/util/function/Function.java @@ -1,8 +1,8 @@ package sandbox.java.util.function; /** - * This is a dummy class that implements just enough of [java.util.function.Function] - * to allow us to compile [sandbox.Task]. + * This is a dummy class that implements just enough of {@link java.util.function.Function} + * to allow us to compile {@link sandbox.Task}. */ @FunctionalInterface public interface Function { diff --git a/djvm/src/main/java/sandbox/java/util/function/Supplier.java b/djvm/src/main/java/sandbox/java/util/function/Supplier.java index 31f236bae6..0ff9f56dfb 100644 --- a/djvm/src/main/java/sandbox/java/util/function/Supplier.java +++ b/djvm/src/main/java/sandbox/java/util/function/Supplier.java @@ -1,8 +1,8 @@ package sandbox.java.util.function; /** - * This is a dummy class that implements just enough of [java.util.function.Supplier] - * to allow us to compile [sandbox.java.lang.ThreadLocal]. + * This is a dummy class that implements just enough of @{link java.util.function.Supplier} + * to allow us to compile {@link sandbox.java.lang.ThreadLocal}. */ @FunctionalInterface public interface Supplier { diff --git a/djvm/src/main/kotlin/net/corda/djvm/SandboxConfiguration.kt b/djvm/src/main/kotlin/net/corda/djvm/SandboxConfiguration.kt index da7cd0d553..ffd233df25 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/SandboxConfiguration.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/SandboxConfiguration.kt @@ -2,6 +2,7 @@ package net.corda.djvm import net.corda.djvm.analysis.AnalysisConfiguration import net.corda.djvm.code.DefinitionProvider +import net.corda.djvm.code.EMIT_TRACING import net.corda.djvm.code.Emitter import net.corda.djvm.execution.ExecutionProfile import net.corda.djvm.rules.Rule @@ -51,7 +52,7 @@ class SandboxConfiguration private constructor( executionProfile = profile, rules = rules, emitters = (emitters ?: Discovery.find()).filter { - enableTracing || !it.isTracer + enableTracing || it.priority > EMIT_TRACING }, definitionProviders = definitionProviders, analysisConfiguration = analysisConfiguration diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt index f8d87fd1ea..ff71421a54 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/AnalysisConfiguration.kt @@ -58,11 +58,21 @@ class AnalysisConfiguration( */ val stitchedInterfaces: Map> get() = STITCHED_INTERFACES + /** + * These classes have extra methods added as they are mapped into the sandbox. + */ + val stitchedClasses: Map> get() = STITCHED_CLASSES + /** * Functionality used to resolve the qualified name and relevant information about a class. */ val classResolver: ClassResolver = ClassResolver(pinnedClasses, TEMPLATE_CLASSES, whitelist, SANDBOX_PREFIX) + /** + * Resolves the internal names of synthetic exception classes. + */ + val exceptionResolver: ExceptionResolver = ExceptionResolver(JVM_EXCEPTIONS, pinnedClasses, SANDBOX_PREFIX) + private val bootstrapClassLoader = bootstrapJar?.let { BootstrapClassLoader(it, classResolver) } val supportingClassLoader = SourceClassLoader(classPath, classResolver, bootstrapClassLoader) @@ -76,11 +86,14 @@ class AnalysisConfiguration( fun isTemplateClass(className: String): Boolean = className in TEMPLATE_CLASSES fun isPinnedClass(className: String): Boolean = className in pinnedClasses + fun isJvmException(className: String): Boolean = className in JVM_EXCEPTIONS + fun isSandboxClass(className: String): Boolean = className.startsWith(SANDBOX_PREFIX) && !isPinnedClass(className) + companion object { /** * The package name prefix to use for classes loaded into a sandbox. */ - private const val SANDBOX_PREFIX: String = "sandbox/" + const val SANDBOX_PREFIX: String = "sandbox/" /** * These class must belong to the application class loader. @@ -111,60 +124,162 @@ class AnalysisConfiguration( java.lang.String.CASE_INSENSITIVE_ORDER::class.java, java.lang.System::class.java, java.lang.ThreadLocal::class.java, + java.lang.Throwable::class.java, kotlin.Any::class.java, sun.misc.JavaLangAccess::class.java, sun.misc.SharedSecrets::class.java ).sandboxed() + setOf( "sandbox/Task", + "sandbox/TaskTypes", "sandbox/java/lang/DJVM", + "sandbox/java/lang/DJVMException", + "sandbox/java/lang/DJVMThrowableWrapper", "sandbox/sun/misc/SharedSecrets\$1", "sandbox/sun/misc/SharedSecrets\$JavaLangAccessImpl" ) + /** + * These are thrown by the JVM itself, and so + * we need to handle them without wrapping them. + * + * Note that this set is closed, i.e. every one + * of these exceptions' [Throwable] super classes + * is also within this set. + * + * The full list of exceptions is determined by: + * hotspot/src/share/vm/classfile/vmSymbols.hpp + */ + val JVM_EXCEPTIONS: Set = setOf( + java.io.IOException::class.java, + java.lang.AbstractMethodError::class.java, + java.lang.ArithmeticException::class.java, + java.lang.ArrayIndexOutOfBoundsException::class.java, + java.lang.ArrayStoreException::class.java, + java.lang.ClassCastException::class.java, + java.lang.ClassCircularityError::class.java, + java.lang.ClassFormatError::class.java, + java.lang.ClassNotFoundException::class.java, + java.lang.CloneNotSupportedException::class.java, + java.lang.Error::class.java, + java.lang.Exception::class.java, + java.lang.ExceptionInInitializerError::class.java, + java.lang.IllegalAccessError::class.java, + java.lang.IllegalAccessException::class.java, + java.lang.IllegalArgumentException::class.java, + java.lang.IllegalStateException::class.java, + java.lang.IncompatibleClassChangeError::class.java, + java.lang.IndexOutOfBoundsException::class.java, + java.lang.InstantiationError::class.java, + java.lang.InstantiationException::class.java, + java.lang.InternalError::class.java, + java.lang.LinkageError::class.java, + java.lang.NegativeArraySizeException::class.java, + java.lang.NoClassDefFoundError::class.java, + java.lang.NoSuchFieldError::class.java, + java.lang.NoSuchFieldException::class.java, + java.lang.NoSuchMethodError::class.java, + java.lang.NoSuchMethodException::class.java, + java.lang.NullPointerException::class.java, + java.lang.OutOfMemoryError::class.java, + java.lang.ReflectiveOperationException::class.java, + java.lang.RuntimeException::class.java, + java.lang.StackOverflowError::class.java, + java.lang.StringIndexOutOfBoundsException::class.java, + java.lang.ThreadDeath::class.java, + java.lang.Throwable::class.java, + java.lang.UnknownError::class.java, + java.lang.UnsatisfiedLinkError::class.java, + java.lang.UnsupportedClassVersionError::class.java, + java.lang.UnsupportedOperationException::class.java, + java.lang.VerifyError::class.java, + java.lang.VirtualMachineError::class.java + ).sandboxed() + setOf( + // Mentioned here to prevent the DJVM from generating a synthetic wrapper. + "sandbox/java/lang/DJVMThrowableWrapper" + ) + /** * These interfaces will be modified as follows when * added to the sandbox: * * interface sandbox.A extends A */ - private val STITCHED_INTERFACES: Map> = mapOf( - sandboxed(CharSequence::class.java) to listOf( - object : MethodBuilder( - access = ACC_PUBLIC or ACC_SYNTHETIC or ACC_BRIDGE, - className = "sandbox/java/lang/CharSequence", - memberName = "subSequence", - descriptor = "(II)Ljava/lang/CharSequence;" - ) { - override fun writeBody(emitter: EmitterModule) = with(emitter) { - pushObject(0) - pushInteger(1) - pushInteger(2) - invokeInterface(className, memberName, "(II)L$className;") - returnObject() - } - }.withBody() - .build(), - MethodBuilder( - access = ACC_PUBLIC or ACC_ABSTRACT, - className = "sandbox/java/lang/CharSequence", - memberName = "toString", - descriptor = "()Ljava/lang/String;" - ).build() - ), + private val STITCHED_INTERFACES: Map> = listOf( + object : MethodBuilder( + access = ACC_PUBLIC or ACC_SYNTHETIC or ACC_BRIDGE, + className = sandboxed(CharSequence::class.java), + memberName = "subSequence", + descriptor = "(II)Ljava/lang/CharSequence;" + ) { + override fun writeBody(emitter: EmitterModule) = with(emitter) { + pushObject(0) + pushInteger(1) + pushInteger(2) + invokeInterface(className, memberName, "(II)L$className;") + returnObject() + } + }.withBody() + .build(), + + MethodBuilder( + access = ACC_PUBLIC or ACC_ABSTRACT, + className = sandboxed(CharSequence::class.java), + memberName = "toString", + descriptor = "()Ljava/lang/String;" + ).build() + ).mapByClassName() + mapOf( sandboxed(Comparable::class.java) to emptyList(), sandboxed(Comparator::class.java) to emptyList(), sandboxed(Iterable::class.java) to emptyList() ) - private fun sandboxed(clazz: Class<*>) = SANDBOX_PREFIX + Type.getInternalName(clazz) + /** + * These classes have extra methods added when mapped into the sandbox. + */ + private val STITCHED_CLASSES: Map> = listOf( + object : MethodBuilder( + access = ACC_FINAL, + className = sandboxed(Enum::class.java), + memberName = "fromDJVM", + descriptor = "()Ljava/lang/Enum;", + signature = "()Ljava/lang/Enum<*>;" + ) { + override fun writeBody(emitter: EmitterModule) = with(emitter) { + pushObject(0) + invokeStatic("sandbox/java/lang/DJVM", "fromDJVMEnum", "(Lsandbox/java/lang/Enum;)Ljava/lang/Enum;") + returnObject() + } + }.withBody() + .build(), + + object : MethodBuilder( + access = ACC_BRIDGE or ACC_SYNTHETIC, + className = sandboxed(Enum::class.java), + memberName = "fromDJVM", + descriptor = "()Ljava/lang/Object;" + ) { + override fun writeBody(emitter: EmitterModule) = with(emitter) { + pushObject(0) + invokeVirtual(className, memberName, "()Ljava/lang/Enum;") + returnObject() + } + }.withBody() + .build() + ).mapByClassName() + + private fun sandboxed(clazz: Class<*>): String = (SANDBOX_PREFIX + Type.getInternalName(clazz)).intern() private fun Set>.sandboxed(): Set = map(Companion::sandboxed).toSet() + private fun Iterable.mapByClassName(): Map> + = groupBy(Member::className).mapValues(Map.Entry>::value) } private open class MethodBuilder( protected val access: Int, protected val className: String, protected val memberName: String, - protected val descriptor: String) { + protected val descriptor: String, + protected val signature: String = "" + ) { private val bodies = mutableListOf() protected open fun writeBody(emitter: EmitterModule) {} @@ -179,7 +294,7 @@ class AnalysisConfiguration( className = className, memberName = memberName, signature = descriptor, - genericsDetails = "", + genericsDetails = signature, body = bodies ) } diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt index 8bfb997ae7..3ab04895e4 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/ClassAndMemberVisitor.kt @@ -2,6 +2,7 @@ package net.corda.djvm.analysis import net.corda.djvm.code.EmitterModule import net.corda.djvm.code.Instruction +import net.corda.djvm.code.emptyAsNull import net.corda.djvm.code.instructions.* import net.corda.djvm.messages.Message import net.corda.djvm.references.* @@ -232,7 +233,7 @@ open class ClassAndMemberVisitor( analysisContext.classes.add(visitedClass) super.visit( version, access, visitedClass.name, signature, - visitedClass.superClass.nullIfEmpty(), + visitedClass.superClass.emptyAsNull, visitedClass.interfaces.toTypedArray() ) } @@ -285,12 +286,19 @@ open class ClassAndMemberVisitor( ): MethodVisitor? { var visitedMember: Member? = null val clazz = currentClass!! - val member = Member(access, clazz.name, name, desc, signature ?: "") + val member = Member( + access = access, + className = clazz.name, + memberName = name, + signature = desc, + genericsDetails = signature ?: "", + exceptions = exceptions?.toMutableSet() ?: mutableSetOf() + ) currentMember = member sourceLocation = sourceLocation.copy( - memberName = name, - signature = desc, - lineNumber = 0 + memberName = name, + signature = desc, + lineNumber = 0 ) val processMember = captureExceptions { visitedMember = visitMethod(clazz, member) @@ -320,12 +328,19 @@ open class ClassAndMemberVisitor( ): FieldVisitor? { var visitedMember: Member? = null val clazz = currentClass!! - val member = Member(access, clazz.name, name, desc, "", value = value) + val member = Member( + access = access, + className = clazz.name, + memberName = name, + signature = desc, + genericsDetails = "", + value = value + ) currentMember = member sourceLocation = sourceLocation.copy( - memberName = name, - signature = desc, - lineNumber = 0 + memberName = name, + signature = desc, + lineNumber = 0 ) val processMember = captureExceptions { visitedMember = visitField(clazz, member) @@ -578,10 +593,6 @@ open class ClassAndMemberVisitor( */ const val API_VERSION: Int = Opcodes.ASM6 - private fun String.nullIfEmpty(): String? { - return if (this.isEmpty()) { null } else { this } - } - } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/ExceptionResolver.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/ExceptionResolver.kt new file mode 100644 index 0000000000..0bfeb103e2 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/ExceptionResolver.kt @@ -0,0 +1,41 @@ +package net.corda.djvm.analysis + +import org.objectweb.asm.Type + +class ExceptionResolver( + private val jvmExceptionClasses: Set, + private val pinnedClasses: Set, + private val sandboxPrefix: String +) { + companion object { + private const val DJVM_EXCEPTION_NAME = "\$1DJVM" + + fun isDJVMException(className: String): Boolean = className.endsWith(DJVM_EXCEPTION_NAME) + fun getDJVMException(className: String): String = className + DJVM_EXCEPTION_NAME + fun getDJVMExceptionOwner(className: String): String = className.dropLast(DJVM_EXCEPTION_NAME.length) + } + + fun getThrowableName(clazz: Class<*>): String { + return getDJVMException(Type.getInternalName(clazz)) + } + + fun getThrowableSuperName(clazz: Class<*>): String { + return getThrowableOwnerName(Type.getInternalName(clazz.superclass)) + } + + fun getThrowableOwnerName(className: String): String { + return if (className in jvmExceptionClasses) { + className.unsandboxed + } else if (className in pinnedClasses) { + className + } else { + getDJVMException(className) + } + } + + private val String.unsandboxed: String get() = if (startsWith(sandboxPrefix)) { + drop(sandboxPrefix.length) + } else { + this + } +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/PrefixTree.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/PrefixTree.kt index a063965b76..26679cc133 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/PrefixTree.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/PrefixTree.kt @@ -35,4 +35,4 @@ class PrefixTree { return false } -} \ No newline at end of file +} diff --git a/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt b/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt index c19cc8111e..ed3fb32e88 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/analysis/Whitelist.kt @@ -100,9 +100,6 @@ open class Whitelist private constructor( "^java/lang/Cloneable(\\..*)?\$".toRegex(), "^java/lang/Object(\\..*)?\$".toRegex(), "^java/lang/Override(\\..*)?\$".toRegex(), - // TODO: sandbox exception handling! - "^java/lang/StackTraceElement\$".toRegex(), - "^java/lang/Throwable\$".toRegex(), "^java/lang/Void\$".toRegex(), "^java/lang/invoke/LambdaMetafactory\$".toRegex(), "^java/lang/invoke/MethodHandles(\\\$.*)?\$".toRegex(), diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt b/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt index 3c800d9859..4d8f2b6307 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/ClassMutator.kt @@ -41,7 +41,11 @@ class ClassMutator( } } - private val emitters: List = emitters + PrependClassInitializer() + /* + * Some emitters must be executed before others. E.g. we need to apply + * the tracing emitters before the non-tracing ones. + */ + private val emitters: List = (emitters + PrependClassInitializer()).sortedBy(Emitter::priority) private val initializers = mutableListOf() /** @@ -128,8 +132,7 @@ class ClassMutator( */ override fun visitInstruction(method: Member, emitter: EmitterModule, instruction: Instruction) { val context = EmitterContext(currentAnalysisContext(), configuration, emitter) - // We need to apply the tracing emitters before the non-tracing ones. - Processor.processEntriesOfType(emitters.sortedByDescending(Emitter::isTracer), analysisContext.messages) { + Processor.processEntriesOfType(emitters, analysisContext.messages) { it.emit(context, instruction) } if (!emitter.emitDefaultInstruction || emitter.hasEmittedCustomCode) { diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt b/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt index f904d276b7..2eb5e0de5d 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/Emitter.kt @@ -18,10 +18,10 @@ interface Emitter { fun emit(context: EmitterContext, instruction: Instruction) /** - * Indication of whether or not the emitter performs instrumentation for tracing inside the sandbox. + * Determines the order in which emitters are executed within the sandbox. */ @JvmDefault - val isTracer: Boolean - get() = false + val priority: Int + get() = EMIT_DEFAULT } \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt b/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt index 2e2d2fc2e4..e51647830b 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/EmitterModule.kt @@ -168,6 +168,14 @@ class EmitterModule( inline fun throwException(message: String) = throwException(T::class.java, message) + /** + * Attempt to cast the object on the top of the stack to the given class. + */ + fun castObjectTo(className: String) { + methodVisitor.visitTypeInsn(CHECKCAST, className) + hasEmittedCustomCode = true + } + /** * Emit instruction for returning from "void" method. */ diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt b/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt index 93a9c5bf7d..3d3b86d2af 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/Types.kt @@ -2,11 +2,22 @@ package net.corda.djvm.code import org.objectweb.asm.Type +import sandbox.java.lang.DJVMException import sandbox.net.corda.djvm.costing.ThresholdViolationError import sandbox.net.corda.djvm.rules.RuleViolationError +/** + * These are the priorities for executing [Emitter] instances. + * Tracing emitters are executed first. + */ +const val EMIT_TRACING: Int = 0 +const val EMIT_TRAPPING_EXCEPTIONS: Int = EMIT_TRACING + 1 +const val EMIT_HANDLING_EXCEPTIONS: Int = EMIT_TRAPPING_EXCEPTIONS + 1 +const val EMIT_DEFAULT: Int = 10 + val ruleViolationError: String = Type.getInternalName(RuleViolationError::class.java) val thresholdViolationError: String = Type.getInternalName(ThresholdViolationError::class.java) +val djvmException: String = Type.getInternalName(DJVMException::class.java) /** * Local extension method for normalizing a class name. diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryBlock.kt b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryBlock.kt new file mode 100644 index 0000000000..a984766d29 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryBlock.kt @@ -0,0 +1,8 @@ +package net.corda.djvm.code.instructions + +import org.objectweb.asm.Label + +open class TryBlock( + val handler: Label, + val typeName: String +) : NoOperationInstruction() \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryCatchBlock.kt b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryCatchBlock.kt index ac9b9e643f..943d745c80 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryCatchBlock.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryCatchBlock.kt @@ -9,6 +9,6 @@ import org.objectweb.asm.Label * @property handler The label of the exception handler. */ class TryCatchBlock( - val typeName: String, - val handler: Label -) : NoOperationInstruction() + typeName: String, + handler: Label +) : TryBlock(handler, typeName) diff --git a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryFinallyBlock.kt b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryFinallyBlock.kt index 808575b05d..7ec2149b73 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryFinallyBlock.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/code/instructions/TryFinallyBlock.kt @@ -7,7 +7,6 @@ import org.objectweb.asm.Label * * @property handler The handler for the finally-block. */ -@Suppress("MemberVisibilityCanBePrivate") class TryFinallyBlock( - val handler: Label -) : NoOperationInstruction() + handler: Label +) : TryBlock(handler, "") diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt index 081bff4fa5..4804074457 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/ClassRewriter.kt @@ -1,7 +1,6 @@ package net.corda.djvm.rewiring import net.corda.djvm.SandboxConfiguration -import net.corda.djvm.analysis.AnalysisConfiguration import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.analysis.ClassAndMemberVisitor.Companion.API_VERSION import net.corda.djvm.code.ClassMutator @@ -11,6 +10,8 @@ import net.corda.djvm.references.Member import net.corda.djvm.utilities.loggerFor import org.objectweb.asm.ClassReader import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.Label +import org.objectweb.asm.MethodVisitor /** * Functionality for rewriting parts of a class as it is being loaded. @@ -22,6 +23,7 @@ open class ClassRewriter( private val configuration: SandboxConfiguration, private val classLoader: ClassLoader ) { + private val analysisConfig = configuration.analysisConfiguration /** * Process class and allow user to rewrite parts/all of its content through provided hooks. @@ -32,13 +34,15 @@ open class ClassRewriter( fun rewrite(reader: ClassReader, context: AnalysisContext): ByteCode { logger.debug("Rewriting class {}...", reader.className) val writer = SandboxClassWriter(reader, classLoader) - val analysisConfiguration = configuration.analysisConfiguration - val classRemapper = SandboxClassRemapper(InterfaceStitcher(writer, analysisConfiguration), analysisConfiguration) + val classRemapper = SandboxClassRemapper( + ClassExceptionRemapper(SandboxStitcher(writer)), + analysisConfig + ) val visitor = ClassMutator( - classRemapper, - analysisConfiguration, - configuration.definitionProviders, - configuration.emitters + classRemapper, + analysisConfig, + configuration.definitionProviders, + configuration.emitters ) visitor.analyze(reader, context, options = ClassReader.EXPAND_FRAMES) return ByteCode(writer.toByteArray(), visitor.hasBeenModified) @@ -50,25 +54,30 @@ open class ClassRewriter( /** * Extra visitor that is applied after [SandboxRemapper]. This "stitches" the original - * unmapped interface as a super-interface of the mapped version. + * unmapped interface as a super-interface of the mapped version, as well as adding + * any extra methods that are needed. */ - private class InterfaceStitcher(parent: ClassVisitor, private val configuration: AnalysisConfiguration) + private inner class SandboxStitcher(parent: ClassVisitor) : ClassVisitor(API_VERSION, parent) { private val extraMethods = mutableListOf() override fun visit(version: Int, access: Int, className: String, signature: String?, superName: String?, interfaces: Array?) { - val stitchedInterfaces = configuration.stitchedInterfaces[className]?.let { methods -> + val stitchedInterfaces = analysisConfig.stitchedInterfaces[className]?.let { methods -> extraMethods += methods - arrayOf(*(interfaces ?: emptyArray()), configuration.classResolver.reverse(className)) + arrayOf(*(interfaces ?: emptyArray()), analysisConfig.classResolver.reverse(className)) } ?: interfaces + analysisConfig.stitchedClasses[className]?.also { methods -> + extraMethods += methods + } + super.visit(version, access, className, signature, superName, stitchedInterfaces) } override fun visitEnd() { for (method in extraMethods) { - method.apply { + with(method) { visitMethod(access, memberName, signature, genericsDetails.emptyAsNull, exceptions.toTypedArray())?.also { mv -> mv.visitCode() EmitterModule(mv).writeByteCode(body) @@ -81,4 +90,26 @@ open class ClassRewriter( super.visitEnd() } } + + /** + * Map exceptions in method signatures to their sandboxed equivalents. + */ + private inner class ClassExceptionRemapper(parent: ClassVisitor) : ClassVisitor(API_VERSION, parent) { + override fun visitMethod(access: Int, name: String, descriptor: String, signature: String?, exceptions: Array?): MethodVisitor? { + val mappedExceptions = exceptions?.map(analysisConfig.exceptionResolver::getThrowableOwnerName)?.toTypedArray() + return super.visitMethod(access, name, descriptor, signature, mappedExceptions)?.let { + MethodExceptionRemapper(it) + } + } + } + + /** + * Map exceptions in method try-catch blocks to their sandboxed equivalents. + */ + private inner class MethodExceptionRemapper(parent: MethodVisitor) : MethodVisitor(API_VERSION, parent) { + override fun visitTryCatchBlock(start: Label, end: Label, handler: Label, exceptionType: String?) { + val mappedExceptionType = exceptionType?.let(analysisConfig.exceptionResolver::getThrowableOwnerName) + super.visitTryCatchBlock(start, end, handler, mappedExceptionType) + } + } } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt index 7f2abccb6a..4dbeae7ab2 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassLoader.kt @@ -3,6 +3,9 @@ package net.corda.djvm.rewiring import net.corda.djvm.SandboxConfiguration import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.analysis.ClassAndMemberVisitor +import net.corda.djvm.analysis.ExceptionResolver.Companion.getDJVMExceptionOwner +import net.corda.djvm.analysis.ExceptionResolver.Companion.isDJVMException +import net.corda.djvm.code.asPackagePath import net.corda.djvm.code.asResourcePath import net.corda.djvm.references.ClassReference import net.corda.djvm.source.ClassSource @@ -33,7 +36,7 @@ class SandboxClassLoader( /** * The analyzer used to traverse the class hierarchy. */ - val analyzer: ClassAndMemberVisitor + private val analyzer: ClassAndMemberVisitor get() = ruleValidator /** @@ -56,6 +59,18 @@ class SandboxClassLoader( */ private val rewriter: ClassRewriter = ClassRewriter(configuration, supportingClassLoader) + /** + * We need to load this class up front, so that we can identify sandboxed exception classes. + */ + private val throwableClass: Class<*> + + init { + // Bootstrap the loading of the sandboxed Throwable class. + loadClassAndBytes(ClassSource.fromClassName("sandbox.java.lang.Object"), context) + loadClassAndBytes(ClassSource.fromClassName("sandbox.java.lang.StackTraceElement"), context) + throwableClass = loadClassAndBytes(ClassSource.fromClassName("sandbox.java.lang.Throwable"), context).type + } + /** * Given a class name, provide its corresponding [LoadedClass] for the sandbox. */ @@ -77,11 +92,43 @@ class SandboxClassLoader( */ @Throws(ClassNotFoundException::class) override fun loadClass(name: String, resolve: Boolean): Class<*> { - val source = ClassSource.fromClassName(name) - return if (name.startsWith("sandbox.") && !analysisConfiguration.isPinnedClass(source.internalClassName)) { - loadClassAndBytes(source, context).type + var clazz = findLoadedClass(name) + if (clazz == null) { + val source = ClassSource.fromClassName(name) + clazz = if (analysisConfiguration.isSandboxClass(source.internalClassName)) { + loadSandboxClass(source, context).type + } else { + super.loadClass(name, resolve) + } + } + if (resolve) { + resolveClass(clazz) + } + return clazz + } + + private fun loadSandboxClass(source: ClassSource, context: AnalysisContext): LoadedClass { + return if (isDJVMException(source.internalClassName)) { + /** + * We need to load a DJVMException's owner class before we can create + * its wrapper exception. And loading the owner should also create the + * wrapper class automatically. + */ + loadedClasses.getOrElse(source.internalClassName) { + loadSandboxClass(ClassSource.fromClassName(getDJVMExceptionOwner(source.qualifiedClassName)), context) + loadedClasses[source.internalClassName] + } ?: throw ClassNotFoundException(source.qualifiedClassName) } else { - super.loadClass(name, resolve) + loadClassAndBytes(source, context).also { clazz -> + /** + * Check whether we've just loaded an unpinned sandboxed throwable class. + * If we have, we may also need to synthesise a throwable wrapper for it. + */ + if (throwableClass.isAssignableFrom(clazz.type) && !analysisConfiguration.isJvmException(source.internalClassName)) { + logger.debug("Generating synthetic throwable for ${source.qualifiedClassName}") + loadWrapperFor(clazz.type) + } + } } } @@ -134,7 +181,7 @@ class SandboxClassLoader( } // Try to define the transformed class. - val clazz = try { + val clazz: Class<*> = try { when { whitelistedClasses.matches(sourceName.asResourcePath) -> supportingClassLoader.loadClass(sourceName) else -> defineClass(resolvedName, byteCode.bytes, 0, byteCode.bytes.size) @@ -167,6 +214,15 @@ class SandboxClassLoader( } } + private fun loadWrapperFor(throwable: Class<*>): LoadedClass { + val className = analysisConfiguration.exceptionResolver.getThrowableName(throwable) + return loadedClasses.getOrPut(className) { + val superName = analysisConfiguration.exceptionResolver.getThrowableSuperName(throwable) + val byteCode = ThrowableWrapperFactory.toByteCode(className, superName) + LoadedClass(defineClass(className.asPackagePath, byteCode.bytes, 0, byteCode.bytes.size), byteCode) + } + } + private companion object { private val logger = loggerFor() private val UNMODIFIED = ByteCode(ByteArray(0), false) diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassRemapper.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassRemapper.kt index 7412999727..9f90981f57 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassRemapper.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/SandboxClassRemapper.kt @@ -3,7 +3,6 @@ package net.corda.djvm.rewiring import net.corda.djvm.analysis.AnalysisConfiguration import net.corda.djvm.analysis.ClassAndMemberVisitor.Companion.API_VERSION import org.objectweb.asm.ClassVisitor -import org.objectweb.asm.Label import org.objectweb.asm.MethodVisitor import org.objectweb.asm.commons.ClassRemapper @@ -35,11 +34,6 @@ class SandboxClassRemapper(cv: ClassVisitor, private val configuration: Analysis return mapperFor(method).visitMethodInsn(opcode, owner, name, descriptor, isInterface) } - override fun visitTryCatchBlock(start: Label, end: Label, handler: Label, type: String?) { - // Don't map caught exception names - these could be thrown by the JVM itself. - nonmapper.visitTryCatchBlock(start, end, handler, type) - } - override fun visitFieldInsn(opcode: Int, owner: String, name: String, descriptor: String) { val field = Element(owner, name, descriptor) return mapperFor(field).visitFieldInsn(opcode, owner, name, descriptor) diff --git a/djvm/src/main/kotlin/net/corda/djvm/rewiring/ThrowableWrapperFactory.kt b/djvm/src/main/kotlin/net/corda/djvm/rewiring/ThrowableWrapperFactory.kt new file mode 100644 index 0000000000..3557ed50cf --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rewiring/ThrowableWrapperFactory.kt @@ -0,0 +1,152 @@ +package net.corda.djvm.rewiring + +import net.corda.djvm.analysis.ExceptionResolver.Companion.isDJVMException +import net.corda.djvm.code.djvmException +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes.* + +/** + * Generates a synthetic [Throwable] class that will wrap a [sandbox.java.lang.Throwable]. + * Only exceptions which are NOT thrown by the JVM will be accompanied one of these. + */ +class ThrowableWrapperFactory( + private val className: String, + private val superName: String +) { + companion object { + const val CONSTRUCTOR_DESCRIPTOR = "(Lsandbox/java/lang/Throwable;)V" + const val FIELD_TYPE = "Lsandbox/java/lang/Throwable;" + const val THROWABLE_FIELD = "t" + + fun toByteCode(className: String, superName: String): ByteCode { + val bytecode: ByteArray = with(ClassWriter(0)) { + ThrowableWrapperFactory(className, superName).accept(this) + toByteArray() + } + return ByteCode(bytecode, true) + } + } + + /** + * Write bytecode for synthetic throwable wrapper class. All of + * these classes implement [sandbox.java.lang.DJVMException], + * either directly or indirectly. + */ + fun accept(writer: ClassWriter) = with(writer) { + if (isDJVMException(superName)) { + childClass() + } else { + baseClass() + } + } + + /** + * This is a "base" wrapper class that inherits from a JVM exception. + * + * + * public class CLASSNAME extends JAVA_EXCEPTION implements DJVMException { + * private final sandbox.java.lang.Throwable t; + * + * public CLASSNAME(sandbox.java.lang.Throwable t) { + * this.t = t; + * } + * + * @Override + * public final sandbox.java.lang.Throwable getThrowable() { + * return t; + * } + * + * @Override + * public final java.lang.Throwable fillInStackTrace() { + * return this; + * } + * } + * + */ + private fun ClassWriter.baseClass() { + // Class definition + visit( + V1_8, + ACC_SYNTHETIC or ACC_PUBLIC, + className, + null, + superName, + arrayOf(djvmException) + ) + + // Private final field to hold the sandbox throwable object. + visitField(ACC_PRIVATE or ACC_FINAL, THROWABLE_FIELD, FIELD_TYPE, null, null) + + // Constructor + visitMethod(ACC_PUBLIC, "", CONSTRUCTOR_DESCRIPTOR, null, null).also { mv -> + mv.visitCode() + mv.visitVarInsn(ALOAD, 0) + mv.visitMethodInsn(INVOKESPECIAL, superName, "", "()V", false) + mv.visitVarInsn(ALOAD, 0) + mv.visitVarInsn(ALOAD, 1) + mv.visitFieldInsn(PUTFIELD, className, THROWABLE_FIELD, FIELD_TYPE) + mv.visitInsn(RETURN) + mv.visitMaxs(2, 2) + mv.visitEnd() + } + + // Getter method for the sandbox throwable object. + visitMethod(ACC_PUBLIC or ACC_FINAL, "getThrowable", "()$FIELD_TYPE", null, null).also { mv -> + mv.visitCode() + mv.visitVarInsn(ALOAD, 0) + mv.visitFieldInsn(GETFIELD, className, THROWABLE_FIELD, FIELD_TYPE) + mv.visitInsn(ARETURN) + mv.visitMaxs(1, 1) + mv.visitEnd() + } + + // Prevent these wrappers from generating their own stack traces. + visitMethod(ACC_PUBLIC or ACC_FINAL, "fillInStackTrace", "()Ljava/lang/Throwable;", null, null).also { mv -> + mv.visitCode() + mv.visitVarInsn(ALOAD, 0) + mv.visitInsn(ARETURN) + mv.visitMaxs(1, 1) + mv.visitEnd() + } + + // End of class + visitEnd() + } + + /** + * This wrapper class inherits from another wrapper class. + * + * + * public class CLASSNAME extends SUPERNAME { + * public CLASSNAME(sandbox.java.lang.Throwable t) { + * super(t); + * } + * } + * + */ + private fun ClassWriter.childClass() { + // Class definition + visit( + V1_8, + ACC_SYNTHETIC or ACC_PUBLIC, + className, + null, + superName, + arrayOf() + ) + + // Constructor + visitMethod(ACC_PUBLIC, "", CONSTRUCTOR_DESCRIPTOR, null, null).also { mv -> + mv.visitCode() + mv.visitVarInsn(ALOAD, 0) + mv.visitVarInsn(ALOAD, 1) + mv.visitMethodInsn(INVOKESPECIAL, superName, "", CONSTRUCTOR_DESCRIPTOR, false) + mv.visitInsn(RETURN) + mv.visitMaxs(2, 2) + mv.visitEnd() + } + + // End of class + visitEnd() + } +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt index a5524ec12b..d898a747b0 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/DisallowCatchingBlacklistedExceptions.kt @@ -27,30 +27,36 @@ class DisallowCatchingBlacklistedExceptions : Emitter { companion object { private val disallowedExceptionTypes = setOf( - ruleViolationError, - thresholdViolationError, + ruleViolationError, + thresholdViolationError, - /** - * These errors indicate that the JVM is failing, - * so don't allow these to be caught either. - */ - "java/lang/StackOverflowError", - "java/lang/OutOfMemoryError", + /** + * These errors indicate that the JVM is failing, + * so don't allow these to be caught either. + */ + "java/lang/StackOverflowError", + "java/lang/OutOfMemoryError", - /** - * These are immediate super-classes for our explicit errors. - */ - "java/lang/VirtualMachineError", - "java/lang/ThreadDeath", + /** + * These are immediate super-classes for our explicit errors. + */ + "java/lang/VirtualMachineError", + "java/lang/ThreadDeath", - /** - * Any of [ThreadDeath] and [VirtualMachineError]'s throwable - * super-classes also need explicit checking. - */ - "java/lang/Throwable", - "java/lang/Error" + /** + * Any of [ThreadDeath] and [VirtualMachineError]'s throwable + * super-classes also need explicit checking. + */ + "java/lang/Throwable", + "java/lang/Error" ) } + /** + * We need to invoke this emitter before the [HandleExceptionUnwrapper] + * so that we don't unwrap exceptions we don't want to catch. + */ + override val priority: Int + get() = EMIT_TRAPPING_EXCEPTIONS } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/HandleExceptionUnwrapper.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/HandleExceptionUnwrapper.kt new file mode 100644 index 0000000000..b616128b85 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/HandleExceptionUnwrapper.kt @@ -0,0 +1,46 @@ +package net.corda.djvm.rules.implementation + +import net.corda.djvm.code.EMIT_HANDLING_EXCEPTIONS +import net.corda.djvm.code.Emitter +import net.corda.djvm.code.EmitterContext +import net.corda.djvm.code.Instruction +import net.corda.djvm.code.instructions.CodeLabel +import net.corda.djvm.code.instructions.TryBlock +import org.objectweb.asm.Label + +/** + * Converts an exception from [java.lang.Throwable] to [sandbox.java.lang.Throwable] + * at the beginning of either a catch block or a finally block. + */ +class HandleExceptionUnwrapper : Emitter { + private val handlers = mutableMapOf() + + override fun emit(context: EmitterContext, instruction: Instruction) = context.emit { + if (instruction is TryBlock) { + handlers[instruction.handler] = instruction.typeName + } else if (instruction is CodeLabel) { + handlers[instruction.label]?.let { exceptionType -> + if (exceptionType.isNotEmpty()) { + /** + * This is a catch block; the wrapping function is allowed to throw exceptions. + */ + invokeStatic("sandbox/java/lang/DJVM", "catch", "(Ljava/lang/Throwable;)Lsandbox/java/lang/Throwable;") + + /** + * When catching exceptions, we also need to tell the verifier which + * which kind of [sandbox.java.lang.Throwable] to expect this to be. + */ + castObjectTo(exceptionType) + } else { + /** + * This is a finally block; the wrapping function MUST NOT throw exceptions. + */ + invokeStatic("sandbox/java/lang/DJVM", "finally", "(Ljava/lang/Throwable;)Lsandbox/java/lang/Throwable;") + } + } + } + } + + override val priority: Int + get() = EMIT_HANDLING_EXCEPTIONS +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/ThrowExceptionWrapper.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/ThrowExceptionWrapper.kt new file mode 100644 index 0000000000..037b4012b0 --- /dev/null +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/ThrowExceptionWrapper.kt @@ -0,0 +1,20 @@ +package net.corda.djvm.rules.implementation + +import net.corda.djvm.code.Emitter +import net.corda.djvm.code.EmitterContext +import net.corda.djvm.code.Instruction +import org.objectweb.asm.Opcodes.ATHROW + +/** + * Converts a [sandbox.java.lang.Throwable] into a [java.lang.Throwable] + * so that the JVM can throw it. + */ +class ThrowExceptionWrapper : Emitter { + override fun emit(context: EmitterContext, instruction: Instruction) = context.emit { + when (instruction.operation) { + ATHROW -> { + invokeStatic("sandbox/java/lang/DJVM", "fromDJVM", "(Lsandbox/java/lang/Throwable;)Ljava/lang/Throwable;") + } + } + } +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceAllocations.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceAllocations.kt index 839ac609bc..a8577c19ac 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceAllocations.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceAllocations.kt @@ -1,5 +1,6 @@ package net.corda.djvm.rules.implementation.instrumentation +import net.corda.djvm.code.EMIT_TRACING import net.corda.djvm.code.Emitter import net.corda.djvm.code.EmitterContext import net.corda.djvm.code.Instruction @@ -40,7 +41,7 @@ class TraceAllocations : Emitter { } } - override val isTracer: Boolean - get() = true + override val priority: Int + get() = EMIT_TRACING } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceInvocations.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceInvocations.kt index b71f1f4657..cafafba1ea 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceInvocations.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceInvocations.kt @@ -1,5 +1,6 @@ package net.corda.djvm.rules.implementation.instrumentation +import net.corda.djvm.code.EMIT_TRACING import net.corda.djvm.code.Emitter import net.corda.djvm.code.EmitterContext import net.corda.djvm.code.Instruction @@ -17,7 +18,7 @@ class TraceInvocations : Emitter { } } - override val isTracer: Boolean - get() = true + override val priority: Int + get() = EMIT_TRACING } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceJumps.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceJumps.kt index 1d7695380b..ce4e41eaa8 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceJumps.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceJumps.kt @@ -1,5 +1,6 @@ package net.corda.djvm.rules.implementation.instrumentation +import net.corda.djvm.code.EMIT_TRACING import net.corda.djvm.code.Emitter import net.corda.djvm.code.EmitterContext import net.corda.djvm.code.Instruction @@ -17,7 +18,7 @@ class TraceJumps : Emitter { } } - override val isTracer: Boolean - get() = true + override val priority: Int + get() = EMIT_TRACING } diff --git a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceThrows.kt b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceThrows.kt index b4756b272e..dc8064ff15 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceThrows.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/rules/implementation/instrumentation/TraceThrows.kt @@ -1,5 +1,6 @@ package net.corda.djvm.rules.implementation.instrumentation +import net.corda.djvm.code.EMIT_TRACING import net.corda.djvm.code.Emitter import net.corda.djvm.code.EmitterContext import net.corda.djvm.code.Instruction @@ -17,7 +18,7 @@ class TraceThrows : Emitter { } } - override val isTracer: Boolean - get() = true + override val priority: Int + get() = EMIT_TRACING } diff --git a/djvm/src/main/kotlin/net/corda/djvm/source/ClassSource.kt b/djvm/src/main/kotlin/net/corda/djvm/source/ClassSource.kt index 99ef5319fb..4cb15b9194 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/source/ClassSource.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/source/ClassSource.kt @@ -21,12 +21,14 @@ class ClassSource private constructor( /** * Instantiate a [ClassSource] from a fully qualified class name. */ + @JvmStatic fun fromClassName(className: String, origin: String? = null) = ClassSource(className, origin) /** * Instantiate a [ClassSource] from a file on disk. */ + @JvmStatic fun fromPath(path: Path) = PathClassSource(path) /** diff --git a/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt b/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt index 8b4789f8df..fbc0f1b0f0 100644 --- a/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt +++ b/djvm/src/main/kotlin/net/corda/djvm/source/SourceClassLoader.kt @@ -2,6 +2,8 @@ package net.corda.djvm.source import net.corda.djvm.analysis.AnalysisContext import net.corda.djvm.analysis.ClassResolver +import net.corda.djvm.analysis.ExceptionResolver.Companion.getDJVMExceptionOwner +import net.corda.djvm.analysis.ExceptionResolver.Companion.isDJVMException import net.corda.djvm.analysis.SourceLocation import net.corda.djvm.code.asResourcePath import net.corda.djvm.messages.Message @@ -61,7 +63,15 @@ abstract class AbstractSourceClassLoader( */ override fun loadClass(name: String, resolve: Boolean): Class<*> { logger.trace("Loading class {}, resolve={}...", name, resolve) - val originalName = classResolver.reverseNormalized(name) + val originalName = classResolver.reverseNormalized(name).let { n -> + // A synthetic exception should be mapped back to its + // corresponding exception in the original hierarchy. + if (isDJVMException(n)) { + getDJVMExceptionOwner(n) + } else { + n + } + } return super.loadClass(originalName, resolve) } diff --git a/djvm/src/main/kotlin/sandbox/Task.kt b/djvm/src/main/kotlin/sandbox/Task.kt index 8a2bbab78a..0be04225bf 100644 --- a/djvm/src/main/kotlin/sandbox/Task.kt +++ b/djvm/src/main/kotlin/sandbox/Task.kt @@ -6,7 +6,10 @@ import sandbox.java.lang.unsandbox typealias SandboxFunction = sandbox.java.util.function.Function -@Suppress("unused") +internal fun isEntryPoint(elt: java.lang.StackTraceElement): Boolean { + return elt.className == "sandbox.Task" && elt.methodName == "apply" +} + class Task(private val function: SandboxFunction?) : SandboxFunction { /** diff --git a/djvm/src/main/kotlin/sandbox/java/lang/DJVM.kt b/djvm/src/main/kotlin/sandbox/java/lang/DJVM.kt index b6a3acdc77..a098d78020 100644 --- a/djvm/src/main/kotlin/sandbox/java/lang/DJVM.kt +++ b/djvm/src/main/kotlin/sandbox/java/lang/DJVM.kt @@ -2,19 +2,25 @@ @file:Suppress("unused") package sandbox.java.lang +import net.corda.djvm.analysis.AnalysisConfiguration.Companion.JVM_EXCEPTIONS +import net.corda.djvm.analysis.ExceptionResolver.Companion.getDJVMException +import net.corda.djvm.rules.implementation.* import org.objectweb.asm.Opcodes.ACC_ENUM +import org.objectweb.asm.Type +import sandbox.isEntryPoint +import sandbox.net.corda.djvm.rules.RuleViolationError private const val SANDBOX_PREFIX = "sandbox." fun Any.unsandbox(): Any { return when (this) { - is Enum<*> -> fromDJVMEnum() is Object -> fromDJVM() is Array<*> -> fromDJVMArray() else -> this } } +@Throws(ClassNotFoundException::class) fun Any.sandbox(): Any { return when (this) { is kotlin.String -> String.toDJVM(this) @@ -27,6 +33,7 @@ fun Any.sandbox(): Any { is kotlin.Double -> Double.toDJVM(this) is kotlin.Boolean -> Boolean.toDJVM(this) is kotlin.Enum<*> -> toDJVMEnum() + is kotlin.Throwable -> toDJVMThrowable() is Array<*> -> toDJVMArray() else -> this } @@ -38,8 +45,11 @@ private fun Array<*>.fromDJVMArray(): Array<*> = Object.fromDJVM(this) * These functions use the "current" classloader, i.e. classloader * that owns this DJVM class. */ -private fun Class<*>.toDJVMType(): Class<*> = Class.forName(name.toSandboxPackage()) -private fun Class<*>.fromDJVMType(): Class<*> = Class.forName(name.fromSandboxPackage()) +@Throws(ClassNotFoundException::class) +internal fun Class<*>.toDJVMType(): Class<*> = Class.forName(name.toSandboxPackage()) + +@Throws(ClassNotFoundException::class) +internal fun Class<*>.fromDJVMType(): Class<*> = Class.forName(name.fromSandboxPackage()) private fun kotlin.String.toSandboxPackage(): kotlin.String { return if (startsWith(SANDBOX_PREFIX)) { @@ -66,10 +76,12 @@ private inline fun Array<*>.toDJVMArray(): Array { } } -private fun Enum<*>.fromDJVMEnum(): kotlin.Enum<*> { +@Throws(ClassNotFoundException::class) +internal fun Enum<*>.fromDJVMEnum(): kotlin.Enum<*> { return javaClass.fromDJVMType().enumConstants[ordinal()] as kotlin.Enum<*> } +@Throws(ClassNotFoundException::class) private fun kotlin.Enum<*>.toDJVMEnum(): Enum<*> { @Suppress("unchecked_cast") return (getEnumConstants(javaClass.toDJVMType() as Class>) as Array>)[ordinal] @@ -87,10 +99,11 @@ fun getEnumConstants(clazz: Class>): Array<*>? { internal fun enumConstantDirectory(clazz: Class>): sandbox.java.util.Map>? { // DO NOT replace get with Kotlin's [] because Kotlin would use java.util.Map. + @Suppress("ReplaceGetOrSet") return allEnumDirectories.get(clazz) ?: createEnumDirectory(clazz) } -@Suppress("unchecked_cast") +@Suppress("unchecked_cast", "ReplaceGetOrSet") internal fun getEnumConstantsShared(clazz: Class>): Array>? { return if (isEnum(clazz)) { // DO NOT replace get with Kotlin's [] because Kotlin would use java.util.Map. @@ -100,7 +113,7 @@ internal fun getEnumConstantsShared(clazz: Class>): Array>): Array>? { return clazz.getMethod("values").let { method -> method.isAccessible = true @@ -109,6 +122,7 @@ private fun createEnum(clazz: Class>): Array>? { }?.apply { allEnums.put(clazz, this) } } +@Suppress("ReplacePutWithAssignment") private fun createEnumDirectory(clazz: Class>): sandbox.java.util.Map> { val universe = getEnumConstantsShared(clazz) ?: throw IllegalArgumentException("${clazz.name} is not an enum type") val directory = sandbox.java.util.LinkedHashMap>(2 * universe.size) @@ -154,5 +168,130 @@ private fun toSandbox(className: kotlin.String): kotlin.String { private val bannedClasses = setOf( "^java\\.lang\\.DJVM(.*)?\$".toRegex(), "^net\\.corda\\.djvm\\..*\$".toRegex(), - "^Task\$".toRegex() + "^Task(.*)?\$".toRegex() ) + +/** + * Exception Management. + * + * This function converts a [sandbox.java.lang.Throwable] into a + * [java.lang.Throwable] that the JVM can actually throw. + */ +fun fromDJVM(t: Throwable?): kotlin.Throwable { + return if (t is DJVMThrowableWrapper) { + // We must be exiting a finally block. + t.fromDJVM() + } else { + try { + /** + * Someone has created a [sandbox.java.lang.Throwable] + * and is (re?)throwing it. + */ + val sandboxedName = t!!.javaClass.name + if (Type.getInternalName(t.javaClass) in JVM_EXCEPTIONS) { + // We map these exceptions to their equivalent JVM classes. + Class.forName(sandboxedName.fromSandboxPackage()).createJavaThrowable(t) + } else { + // Whereas the sandbox creates a synthetic throwable wrapper for these. + Class.forName(getDJVMException(sandboxedName)) + .getDeclaredConstructor(sandboxThrowable) + .newInstance(t) as kotlin.Throwable + } + } catch (e: Exception) { + RuleViolationError(e.message) + } + } +} + +/** + * Wraps a [java.lang.Throwable] inside a [sandbox.java.lang.Throwable]. + * This function is invoked at the beginning of a finally block, and + * so does not need to return a reference to the equivalent sandboxed + * exception. The finally block only needs to be able to re-throw the + * original exception when it finishes. + */ +fun finally(t: kotlin.Throwable): Throwable = DJVMThrowableWrapper(t) + +/** + * Converts a [java.lang.Throwable] into a [sandbox.java.lang.Throwable]. + * It is invoked at the start of each catch block. + * + * Note: [DisallowCatchingBlacklistedExceptions] means that we don't + * need to handle [ThreadDeath] here. + */ +fun catch(t: kotlin.Throwable): Throwable { + try { + return t.toDJVMThrowable() + } catch (e: Exception) { + throw RuleViolationError(e.message) + } +} + +/** + * Worker functions to convert [java.lang.Throwable] into [sandbox.java.lang.Throwable]. + */ +private fun kotlin.Throwable.toDJVMThrowable(): Throwable { + return (this as? DJVMException)?.getThrowable() ?: javaClass.toDJVMType().createDJVMThrowable(this) +} + +/** + * Creates a new [sandbox.java.lang.Throwable] from a [java.lang.Throwable], + * which was probably thrown by the JVM itself. + */ +private fun Class<*>.createDJVMThrowable(t: kotlin.Throwable): Throwable { + return (try { + getDeclaredConstructor(String::class.java).newInstance(String.toDJVM(t.message)) + } catch (e: NoSuchMethodException) { + newInstance() + } as Throwable).apply { + t.cause?.also { + initCause(it.toDJVMThrowable()) + } + stackTrace = sanitiseToDJVM(t.stackTrace) + } +} + +private fun Class<*>.createJavaThrowable(t: Throwable): kotlin.Throwable { + return (try { + getDeclaredConstructor(kotlin.String::class.java).newInstance(String.fromDJVM(t.message)) + } catch (e: NoSuchMethodException) { + newInstance() + } as kotlin.Throwable).apply { + t.cause?.also { + initCause(fromDJVM(it)) + } + stackTrace = copyFromDJVM(t.stackTrace) + } +} + +private fun sanitiseToDJVM(source: Array): Array { + var idx = 0 + while (idx < source.size && !isEntryPoint(source[idx])) { + ++idx + } + return copyToDJVM(source, 0, idx) +} + +internal fun copyToDJVM(source: Array, fromIdx: Int, toIdx: Int): Array { + return source.sliceArray(fromIdx until toIdx).map(::toDJVM).toTypedArray() +} + +private fun toDJVM(elt: java.lang.StackTraceElement) = StackTraceElement( + String.toDJVM(elt.className), + String.toDJVM(elt.methodName), + String.toDJVM(elt.fileName), + elt.lineNumber +) + +private fun copyFromDJVM(source: Array): Array { + return source.map(::fromDJVM).toTypedArray() +} + +private fun fromDJVM(elt: StackTraceElement) = java.lang.StackTraceElement( + String.fromDJVM(elt.className), + String.fromDJVM(elt.methodName), + String.fromDJVM(elt.fileName), + elt.lineNumber +) + +private val sandboxThrowable: Class<*> = Throwable::class.java diff --git a/djvm/src/main/kotlin/sandbox/java/lang/DJVMException.kt b/djvm/src/main/kotlin/sandbox/java/lang/DJVMException.kt new file mode 100644 index 0000000000..553e8533ab --- /dev/null +++ b/djvm/src/main/kotlin/sandbox/java/lang/DJVMException.kt @@ -0,0 +1,12 @@ +package sandbox.java.lang + +/** + * All synthetic [Throwable] classes wrapping non-JVM exceptions + * will implement this interface. + */ +interface DJVMException { + /** + * Returns the [sandbox.java.lang.Throwable] instance inside the wrapper. + */ + fun getThrowable(): Throwable +} \ No newline at end of file diff --git a/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/ThresholdViolationError.kt b/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/ThresholdViolationError.kt index 0fe4283caf..b312a4091f 100644 --- a/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/ThresholdViolationError.kt +++ b/djvm/src/main/kotlin/sandbox/net/corda/djvm/costing/ThresholdViolationError.kt @@ -6,4 +6,4 @@ package sandbox.net.corda.djvm.costing * * @property message The description of the condition causing the problem. */ -class ThresholdViolationError(override val message: String) : ThreadDeath() +class ThresholdViolationError(override val message: String?) : ThreadDeath() diff --git a/djvm/src/main/kotlin/sandbox/net/corda/djvm/rules/RuleViolationError.kt b/djvm/src/main/kotlin/sandbox/net/corda/djvm/rules/RuleViolationError.kt index 24b0e73775..1bd39bdf39 100644 --- a/djvm/src/main/kotlin/sandbox/net/corda/djvm/rules/RuleViolationError.kt +++ b/djvm/src/main/kotlin/sandbox/net/corda/djvm/rules/RuleViolationError.kt @@ -7,4 +7,4 @@ package sandbox.net.corda.djvm.rules * * @property message The description of the condition causing the problem. */ -class RuleViolationError(override val message: String) : ThreadDeath() \ No newline at end of file +class RuleViolationError(override val message: String?) : ThreadDeath() \ No newline at end of file diff --git a/djvm/src/test/java/net/corda/djvm/WithJava.java b/djvm/src/test/java/net/corda/djvm/WithJava.java new file mode 100644 index 0000000000..3e8cf05145 --- /dev/null +++ b/djvm/src/test/java/net/corda/djvm/WithJava.java @@ -0,0 +1,24 @@ +package net.corda.djvm; + +import net.corda.djvm.execution.ExecutionSummaryWithResult; +import net.corda.djvm.execution.SandboxExecutor; +import net.corda.djvm.source.ClassSource; + +import java.util.function.Function; + +public interface WithJava { + + static ExecutionSummaryWithResult run( + SandboxExecutor executor, Class> task, T input) { + try { + return executor.run(ClassSource.fromClassName(task.getName(), null), input); + } catch (Exception e) { + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else { + throw new RuntimeException(e.getMessage(), e); + } + } + } + +} diff --git a/djvm/src/test/java/net/corda/djvm/execution/SandboxEnumJavaTest.java b/djvm/src/test/java/net/corda/djvm/execution/SandboxEnumJavaTest.java new file mode 100644 index 0000000000..0343d9517d --- /dev/null +++ b/djvm/src/test/java/net/corda/djvm/execution/SandboxEnumJavaTest.java @@ -0,0 +1,106 @@ +package net.corda.djvm.execution; + +import net.corda.djvm.TestBase; +import net.corda.djvm.WithJava; +import static net.corda.djvm.messages.Severity.*; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import org.junit.Test; + +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; + +import static java.util.Collections.emptySet; + +public class SandboxEnumJavaTest extends TestBase { + + @Test + public void testEnumInsideSandbox() { + sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> { + SandboxExecutor executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + ExecutionSummaryWithResult output = WithJava.run(executor, TransformEnum.class, 0); + assertThat(output.getResult()) + .isEqualTo(new String[]{ "ONE", "TWO", "THREE" }); + return null; + }); + } + + @Test + public void testReturnEnumFromSandbox() { + sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> { + SandboxExecutor executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + ExecutionSummaryWithResult output = WithJava.run(executor, FetchEnum.class, "THREE"); + assertThat(output.getResult()) + .isEqualTo(ExampleEnum.THREE); + return null; + }); + } + + @Test + public void testWeCanIdentifyClassAsEnum() { + sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> { + SandboxExecutor executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + ExecutionSummaryWithResult output = WithJava.run(executor, AssertEnum.class, ExampleEnum.THREE); + assertThat(output.getResult()).isTrue(); + return null; + }); + } + + @Test + public void testWeCanCreateEnumMap() { + sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> { + SandboxExecutor executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + ExecutionSummaryWithResult output = WithJava.run(executor, UseEnumMap.class, ExampleEnum.TWO); + assertThat(output.getResult()).isEqualTo(1); + return null; + }); + } + + @Test + public void testWeCanCreateEnumSet() { + sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> { + SandboxExecutor executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + ExecutionSummaryWithResult output = WithJava.run(executor, UseEnumSet.class, ExampleEnum.ONE); + assertThat(output.getResult()).isTrue(); + return null; + }); + } + + public static class AssertEnum implements Function { + @Override + public Boolean apply(ExampleEnum input) { + return input.getClass().isEnum(); + } + } + + public static class TransformEnum implements Function { + @Override + public String[] apply(Integer input) { + return Stream.of(ExampleEnum.values()).map(ExampleEnum::name).toArray(String[]::new); + } + } + + public static class FetchEnum implements Function { + public ExampleEnum apply(String input) { + return ExampleEnum.valueOf(input); + } + } + + public static class UseEnumMap implements Function { + @Override + public Integer apply(ExampleEnum input) { + Map map = new EnumMap<>(ExampleEnum.class); + map.put(input, input.name()); + return map.size(); + } + } + + public static class UseEnumSet implements Function { + @Override + public Boolean apply(ExampleEnum input) { + return EnumSet.allOf(ExampleEnum.class).contains(input); + } + } +} diff --git a/djvm/src/test/java/net/corda/djvm/execution/SandboxThrowableJavaTest.java b/djvm/src/test/java/net/corda/djvm/execution/SandboxThrowableJavaTest.java new file mode 100644 index 0000000000..26203f641b --- /dev/null +++ b/djvm/src/test/java/net/corda/djvm/execution/SandboxThrowableJavaTest.java @@ -0,0 +1,79 @@ +package net.corda.djvm.execution; + +import net.corda.djvm.TestBase; +import net.corda.djvm.WithJava; +import static net.corda.djvm.messages.Severity.*; + +import org.junit.Test; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.LinkedList; +import java.util.List; +import java.util.function.Function; + +import static java.util.Collections.emptySet; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class SandboxThrowableJavaTest extends TestBase { + + @Test + public void testUserExceptionHandling() { + sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> { + SandboxExecutor executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + ExecutionSummaryWithResult output = WithJava.run(executor, ThrowAndCatchJavaExample.class, "Hello World!"); + assertThat(output.getResult()) + .isEqualTo(new String[]{ "FIRST FINALLY", "BASE EXCEPTION", "Hello World!", "SECOND FINALLY" }); + return null; + }); + } + + @Test + public void testCheckedExceptions() { + sandbox(new Object[]{ DEFAULT }, emptySet(), WARNING, true, ctx -> { + SandboxExecutor executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration()); + + ExecutionSummaryWithResult success = WithJava.run(executor, JavaWithCheckedExceptions.class, "http://localhost:8080/hello/world"); + assertThat(success.getResult()).isEqualTo("/hello/world"); + + ExecutionSummaryWithResult failure = WithJava.run(executor, JavaWithCheckedExceptions.class, "nasty string"); + assertThat(failure.getResult()).isEqualTo("CATCH:Illegal character in path at index 5: nasty string"); + + return null; + }); + } + + public static class ThrowAndCatchJavaExample implements Function { + @Override + public String[] apply(String input) { + List data = new LinkedList<>(); + try { + try { + throw new MyExampleException(input); + } finally { + data.add("FIRST FINALLY"); + } + } catch (MyBaseException e) { + data.add("BASE EXCEPTION"); + data.add(e.getMessage()); + } catch (Exception e) { + data.add("NOT THIS ONE!"); + } finally { + data.add("SECOND FINALLY"); + } + + return data.toArray(new String[0]); + } + } + + public static class JavaWithCheckedExceptions implements Function { + @Override + public String apply(String input) { + try { + return new URI(input).getPath(); + } catch (URISyntaxException e) { + return "CATCH:" + e.getMessage(); + } + } + } +} diff --git a/djvm/src/test/kotlin/net/corda/djvm/DJVMExceptionTest.kt b/djvm/src/test/kotlin/net/corda/djvm/DJVMExceptionTest.kt new file mode 100644 index 0000000000..886b1efacc --- /dev/null +++ b/djvm/src/test/kotlin/net/corda/djvm/DJVMExceptionTest.kt @@ -0,0 +1,100 @@ +package net.corda.djvm + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.junit.Test +import sandbox.SandboxFunction +import sandbox.Task +import sandbox.java.lang.sandbox + +class DJVMExceptionTest { + @Test + fun testSingleException() { + val result = Task(SingleExceptionTask()).apply("Hello World") + assertThat(result).isInstanceOf(Throwable::class.java) + result as Throwable + + assertThat(result.message).isEqualTo("Hello World") + assertThat(result.cause).isNull() + assertThat(result.stackTrace) + .hasSize(2) + .allSatisfy { it is StackTraceElement && it.className == result.javaClass.name } + } + + @Test + fun testMultipleExceptions() { + val result = Task(MultipleExceptionsTask()).apply("Hello World") + assertThat(result).isInstanceOf(Throwable::class.java) + result as Throwable + + assertThat(result.message).isEqualTo("Hello World(1)(2)") + assertThat(result.cause).isInstanceOf(Throwable::class.java) + assertThat(result.stackTrace) + .hasSize(2) + .allSatisfy { it is StackTraceElement && it.className == result.javaClass.name } + val resultLineNumbers = result.stackTrace.toLineNumbers() + + val firstCause = result.cause as Throwable + assertThat(firstCause.message).isEqualTo("Hello World(1)") + assertThat(firstCause.cause).isInstanceOf(Throwable::class.java) + assertThat(firstCause.stackTrace) + .hasSize(2) + .allSatisfy { it is StackTraceElement && it.className == result.javaClass.name } + val firstCauseLineNumbers = firstCause.stackTrace.toLineNumbers() + + val rootCause = firstCause.cause as Throwable + assertThat(rootCause.message).isEqualTo("Hello World") + assertThat(rootCause.cause).isNull() + assertThat(rootCause.stackTrace) + .hasSize(2) + .allSatisfy { it is StackTraceElement && it.className == result.javaClass.name } + val rootCauseLineNumbers = rootCause.stackTrace.toLineNumbers() + + // These stack traces should share one line number and have one distinct line number each. + assertThat(resultLineNumbers.toSet() + firstCauseLineNumbers.toSet() + rootCauseLineNumbers.toSet()) + .hasSize(4) + } + + @Test + fun testJavaThrowableToSandbox() { + val result = Throwable("Hello World").sandbox() + assertThat(result).isInstanceOf(sandbox.java.lang.Throwable::class.java) + result as sandbox.java.lang.Throwable + + assertThat(result.message).isEqualTo("Hello World".toDJVM()) + assertThat(result.stackTrace).isNotEmpty() + assertThat(result.cause).isNull() + } + + @Test + fun testWeTryToCreateCorrectSandboxExceptionsAtRuntime() { + assertThatExceptionOfType(ClassNotFoundException::class.java) + .isThrownBy { Exception("Hello World").sandbox() } + .withMessage("sandbox.java.lang.Exception") + assertThatExceptionOfType(ClassNotFoundException::class.java) + .isThrownBy { RuntimeException("Hello World").sandbox() } + .withMessage("sandbox.java.lang.RuntimeException") + } +} + +class SingleExceptionTask : SandboxFunction { + override fun apply(input: Any?): sandbox.java.lang.Throwable? { + return sandbox.java.lang.Throwable(input as? sandbox.java.lang.String) + } +} + +class MultipleExceptionsTask : SandboxFunction { + override fun apply(input: Any?): sandbox.java.lang.Throwable? { + val root = sandbox.java.lang.Throwable(input as? sandbox.java.lang.String) + val nested = sandbox.java.lang.Throwable(root.message + "(1)", root) + return sandbox.java.lang.Throwable(nested.message + "(2)", nested) + } +} + +private infix operator fun sandbox.java.lang.String.plus(s: String): sandbox.java.lang.String { + return (toString() + s).toDJVM() +} + +private fun Array.toLineNumbers(): IntArray { + return map(StackTraceElement::getLineNumber).toIntArray() +} \ No newline at end of file diff --git a/djvm/src/test/kotlin/net/corda/djvm/DJVMTest.kt b/djvm/src/test/kotlin/net/corda/djvm/DJVMTest.kt index d71fa1a36a..37048ee7f2 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/DJVMTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/DJVMTest.kt @@ -113,14 +113,4 @@ class DJVMTest { assertArrayEquals(ByteArray(1) { 127.toByte() }, result[9] as ByteArray) assertArrayEquals(CharArray(1) { '?' }, result[10] as CharArray) } - - private fun String.toDJVM(): sandbox.java.lang.String = sandbox.java.lang.String.toDJVM(this) - private fun Long.toDJVM(): sandbox.java.lang.Long = sandbox.java.lang.Long.toDJVM(this) - private fun Int.toDJVM(): sandbox.java.lang.Integer = sandbox.java.lang.Integer.toDJVM(this) - private fun Short.toDJVM(): sandbox.java.lang.Short = sandbox.java.lang.Short.toDJVM(this) - private fun Byte.toDJVM(): sandbox.java.lang.Byte = sandbox.java.lang.Byte.toDJVM(this) - private fun Float.toDJVM(): sandbox.java.lang.Float = sandbox.java.lang.Float.toDJVM(this) - private fun Double.toDJVM(): sandbox.java.lang.Double = sandbox.java.lang.Double.toDJVM(this) - private fun Char.toDJVM(): sandbox.java.lang.Character = sandbox.java.lang.Character.toDJVM(this) - private fun Boolean.toDJVM(): sandbox.java.lang.Boolean = sandbox.java.lang.Boolean.toDJVM(this) } \ No newline at end of file diff --git a/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt b/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt index a771798655..ad16eee53a 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/TestBase.kt @@ -37,22 +37,29 @@ abstract class TestBase { val ALL_EMITTERS = Discovery.find() // We need at least these emitters to handle the Java API classes. + @JvmField val BASIC_EMITTERS: List = listOf( ArgumentUnwrapper(), + HandleExceptionUnwrapper(), ReturnTypeWrapper(), RewriteClassMethods(), - StringConstantWrapper() + StringConstantWrapper(), + ThrowExceptionWrapper() ) val ALL_DEFINITION_PROVIDERS = Discovery.find() // We need at least these providers to handle the Java API classes. + @JvmField val BASIC_DEFINITION_PROVIDERS: List = listOf(StaticConstantRemover()) + @JvmField val BLANK = emptySet() + @JvmField val DEFAULT = (ALL_RULES + ALL_EMITTERS + ALL_DEFINITION_PROVIDERS).distinctBy(Any::javaClass) + @JvmField val DETERMINISTIC_RT: Path = Paths.get( System.getProperty("deterministic-rt.path") ?: throw AssertionError("deterministic-rt.path property not set")) @@ -89,7 +96,7 @@ abstract class TestBase { val reader = ClassReader(T::class.java.name) AnalysisConfiguration( minimumSeverityLevel = minimumSeverityLevel, - classPath = listOf(DETERMINISTIC_RT) + bootstrapJar = DETERMINISTIC_RT ).use { analysisConfiguration -> val validator = RuleValidator(ALL_RULES, analysisConfiguration) val context = AnalysisContext.fromConfiguration(analysisConfiguration) diff --git a/djvm/src/test/kotlin/net/corda/djvm/Utilities.kt b/djvm/src/test/kotlin/net/corda/djvm/Utilities.kt index d493238723..6313661b0c 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/Utilities.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/Utilities.kt @@ -1,22 +1,24 @@ +@file:JvmName("UtilityFunctions") package net.corda.djvm import sandbox.net.corda.djvm.costing.ThresholdViolationError import sandbox.net.corda.djvm.rules.RuleViolationError +/** + * Allows us to create a [Utilities] object that we can pin inside the sandbox. + */ object Utilities { fun throwRuleViolationError(): Nothing = throw RuleViolationError("Can't catch this!") fun throwThresholdViolationError(): Nothing = throw ThresholdViolationError("Can't catch this!") - - fun throwContractConstraintViolation(): Nothing = throw IllegalArgumentException("Contract constraint violated") - - fun throwError(): Nothing = throw Error() - - fun throwThrowable(): Nothing = throw Throwable() - - fun throwThreadDeath(): Nothing = throw ThreadDeath() - - fun throwStackOverflowError(): Nothing = throw StackOverflowError("FAKE OVERFLOW!") - - fun throwOutOfMemoryError(): Nothing = throw OutOfMemoryError("FAKE OOM!") } + +fun String.toDJVM(): sandbox.java.lang.String = sandbox.java.lang.String.toDJVM(this) +fun Long.toDJVM(): sandbox.java.lang.Long = sandbox.java.lang.Long.toDJVM(this) +fun Int.toDJVM(): sandbox.java.lang.Integer = sandbox.java.lang.Integer.toDJVM(this) +fun Short.toDJVM(): sandbox.java.lang.Short = sandbox.java.lang.Short.toDJVM(this) +fun Byte.toDJVM(): sandbox.java.lang.Byte = sandbox.java.lang.Byte.toDJVM(this) +fun Float.toDJVM(): sandbox.java.lang.Float = sandbox.java.lang.Float.toDJVM(this) +fun Double.toDJVM(): sandbox.java.lang.Double = sandbox.java.lang.Double.toDJVM(this) +fun Char.toDJVM(): sandbox.java.lang.Character = sandbox.java.lang.Character.toDJVM(this) +fun Boolean.toDJVM(): sandbox.java.lang.Boolean = sandbox.java.lang.Boolean.toDJVM(this) \ No newline at end of file diff --git a/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt index a3919c964c..32fa876195 100644 --- a/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt +++ b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxExecutorTest.kt @@ -6,14 +6,8 @@ import foo.bar.sandbox.toNumber import net.corda.djvm.TestBase import net.corda.djvm.analysis.Whitelist import net.corda.djvm.Utilities -import net.corda.djvm.Utilities.throwContractConstraintViolation -import net.corda.djvm.Utilities.throwError -import net.corda.djvm.Utilities.throwOutOfMemoryError import net.corda.djvm.Utilities.throwRuleViolationError -import net.corda.djvm.Utilities.throwStackOverflowError -import net.corda.djvm.Utilities.throwThreadDeath import net.corda.djvm.Utilities.throwThresholdViolationError -import net.corda.djvm.Utilities.throwThrowable import net.corda.djvm.assertions.AssertionExtensions.withProblem import net.corda.djvm.rewiring.SandboxClassLoadingException import org.assertj.core.api.Assertions.assertThat @@ -55,7 +49,7 @@ class SandboxExecutorTest : TestBase() { class Contract : Function { override fun apply(input: Transaction) { - throwContractConstraintViolation() + throw IllegalArgumentException("Contract constraint violated") } } @@ -74,11 +68,7 @@ class SandboxExecutorTest : TestBase() { val obj = Object() val hash1 = obj.hashCode() val hash2 = obj.hashCode() - //require(hash1 == hash2) - // TODO: Replace require() once we have working exception support. - if (hash1 != hash2) { - throwError() - } + require(hash1 == hash2) return Object().hashCode() } } @@ -180,7 +170,7 @@ class SandboxExecutorTest : TestBase() { class TestCatchThreadDeath : Function { override fun apply(input: Int): Int { return try { - throwThreadDeath() + throw ThreadDeath() } catch (exception: ThreadDeath) { 1 } @@ -261,8 +251,8 @@ class SandboxExecutorTest : TestBase() { override fun apply(input: Int): Int { return try { when (input) { - 1 -> throwThrowable() - 2 -> throwError() + 1 -> throw Throwable() + 2 -> throw Error() else -> 0 } } catch (exception: Error) { @@ -277,20 +267,20 @@ class SandboxExecutorTest : TestBase() { override fun apply(input: Int): Int { return try { when (input) { - 1 -> throwThrowable() - 2 -> throwError() + 1 -> throw Throwable() + 2 -> throw Error() 3 -> try { - throwThreadDeath() + throw ThreadDeath() } catch (ex: ThreadDeath) { 3 } 4 -> try { - throwStackOverflowError() + throw StackOverflowError("FAKE OVERFLOW!") } catch (ex: StackOverflowError) { 4 } 5 -> try { - throwOutOfMemoryError() + throw OutOfMemoryError("FAKE OOM!") } catch (ex: OutOfMemoryError) { 5 } diff --git a/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxThrowableTest.kt b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxThrowableTest.kt new file mode 100644 index 0000000000..ae013a9c1e --- /dev/null +++ b/djvm/src/test/kotlin/net/corda/djvm/execution/SandboxThrowableTest.kt @@ -0,0 +1,95 @@ +package net.corda.djvm.execution + +import net.corda.djvm.TestBase +import org.assertj.core.api.Assertions.* +import org.junit.Test +import java.util.function.Function + +class SandboxThrowableTest : TestBase() { + + @Test + fun `test user exception handling`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor>(configuration) + contractExecutor.run("Hello World").apply { + assertThat(result) + .isEqualTo(arrayOf("FIRST FINALLY", "BASE EXCEPTION", "Hello World", "SECOND FINALLY")) + } + } + + @Test + fun `test rethrowing an exception`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor>(configuration) + contractExecutor.run("Hello World").apply { + assertThat(result) + .isEqualTo(arrayOf("FIRST CATCH", "FIRST FINALLY", "SECOND CATCH", "Hello World", "SECOND FINALLY")) + } + } + + @Test + fun `test JVM exceptions still propagate`() = sandbox(DEFAULT) { + val contractExecutor = DeterministicSandboxExecutor(configuration) + contractExecutor.run(-1).apply { + assertThat(result) + .isEqualTo("sandbox.java.lang.ArrayIndexOutOfBoundsException:-1") + } + } +} + +class ThrowAndRethrowExample : Function> { + override fun apply(input: String): Array { + val data = mutableListOf() + try { + try { + throw MyExampleException(input) + } catch (e: Exception) { + data += "FIRST CATCH" + throw e + } finally { + data += "FIRST FINALLY" + } + } catch (e: MyExampleException) { + data += "SECOND CATCH" + e.message?.apply { data += this } + } finally { + data += "SECOND FINALLY" + } + + return data.toTypedArray() + } +} + +class ThrowAndCatchExample : Function> { + override fun apply(input: String): Array { + val data = mutableListOf() + try { + try { + throw MyExampleException(input) + } finally { + data += "FIRST FINALLY" + } + } catch (e: MyBaseException) { + data += "BASE EXCEPTION" + e.message?.apply { data += this } + } catch (e: Exception) { + data += "NOT THIS ONE!" + } finally { + data += "SECOND FINALLY" + } + + return data.toTypedArray() + } +} + +class TriggerJVMException : Function { + override fun apply(input: Int): String { + return try { + arrayOf(0, 1, 2)[input] + "No Error" + } catch (e: Exception) { + e.javaClass.name + ':' + (e.message ?: "") + } + } +} + +open class MyBaseException(message: String) : Exception(message) +class MyExampleException(message: String) : MyBaseException(message) \ No newline at end of file diff --git a/docs/source/api-states.rst b/docs/source/api-states.rst index ad05640b16..156a38fcd7 100644 --- a/docs/source/api-states.rst +++ b/docs/source/api-states.rst @@ -100,6 +100,37 @@ Because ``OwnableState`` models fungible assets that can be merged and split ove not have a ``linearId``. $5 of cash created by one transaction is considered to be identical to $5 of cash produced by another transaction. +FungibleState +~~~~~~~~~~~~~ + +`FungibleState` is an interface to represent things which are fungible, this means that there is an expectation that +these things can be split and merged. That's the only assumption made by this interface. This interface should be +implemented if you want to represent fractional ownership in a thing, or if you have many things. Examples: + +* There is only one Mona Lisa which you wish to issue 100 tokens, each representing a 1% interest in the Mona Lisa +* A company issues 1000 shares with a nominal value of 1, in one batch of 1000. This means the single batch of 1000 + shares could be split up into 1000 units of 1 share. + +The interface is defined as follows: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/FungibleState.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + +As seen, the interface takes a type parameter `T` that represents the fungible thing in question. This should describe +the basic type of the asset e.g. GBP, USD, oil, shares in company , etc. and any additional metadata (issuer, grade, +class, etc.). An upper-bound is not specified for `T` to ensure flexibility. Typically, a class would be provided that +implements `TokenizableAssetInfo` so the thing can be easily added and subtracted using the `Amount` class. + +This interface has been added in addition to `FungibleAsset` to provide some additional flexibility which +`FungibleAsset` lacks, in particular: +* `FungibleAsset` defines an amount property of type Amount>, therefore there is an assumption that all + fungible things are issued by a single well known party but this is not always the case. +* `FungibleAsset` implements `OwnableState`, as such there is an assumption that all fungible things are ownable. + Other interfaces ^^^^^^^^^^^^^^^^ You can also customize your state by implementing the following interfaces: diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 45c517f6d8..e67802aa20 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -225,6 +225,15 @@ Unreleased normal state when it occurs in an input or output position. *This feature is only available on Corda networks running with a minimum platform version of 4.* +* Removed type parameter `U` from `tryLockFungibleStatesForSpending` to allow the function to be used with `FungibleState` + as well as `FungibleAsset`. This _might_ cause a compile failure in some obscure cases due to the removal of the type + parameter from the method. If your CorDapp does specify types explicitly when using this method then updating the types + will allow your app to compile successfully. However, those using type inference (e.g. using Kotlin) should not experience + any changes. Old CorDapp JARs will still work regardless. + +* `issuer_ref` column in `FungibleStateSchema` was updated to be nullable to support the introduction of the + `FungibleState` interface. The `vault_fungible_states` table can hold both `FungibleAssets` and `FungibleStates`. + Version 3.3 ----------- diff --git a/docs/source/joining-a-network.rst b/docs/source/compatibility-zones.rst similarity index 82% rename from docs/source/joining-a-network.rst rename to docs/source/compatibility-zones.rst index 6de161b781..eb3e6680b2 100644 --- a/docs/source/joining-a-network.rst +++ b/docs/source/compatibility-zones.rst @@ -4,12 +4,15 @@ -Connecting to a compatibility zone -================================== +Compatibility zones +=================== -Every Corda node is part of a network (also called a zone) that is *permissioned*. Production deployments require a -secure certificate authority. Most users will join an existing network such as the main Corda network or the Corda -TestNet. +Every Corda node is part of a "zone" (also sometimes called a Corda network) that is *permissioned*. Production +deployments require a secure certificate authority. Most users will join an existing network such as Corda +Network (the main network) or the Corda Testnet. We use the term "zone" to refer to a set of technically compatible nodes reachable +over a TCP/IP network like the internet. The word "network" is used in Corda but can be ambiguous with the concept +of a "business network", which is usually more like a membership list or subset of nodes in a zone that have agreed +to trade with each other. To connect to a compatibility zone you need to register with its certificate signing authority (doorman) by submitting a certificate signing request (CSR) to obtain a valid identity for the zone. You could do this out of band, for instance diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index bb76acadc8..fe4e7455b8 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -213,6 +213,7 @@ absolute path to the node's base directory. :doormanURL: Root address of the network registration service. :networkMapURL: Root address of the network map service. + :pnm: Optional UUID of the private network operating within the compatibility zone this node should be joinging. .. note:: Only one of ``compatibilityZoneURL`` or ``networkServices`` should be used. diff --git a/docs/source/corda-networks-index.rst b/docs/source/corda-networks-index.rst index f42f296cb5..2dbcea2a19 100644 --- a/docs/source/corda-networks-index.rst +++ b/docs/source/corda-networks-index.rst @@ -4,8 +4,8 @@ Networks .. toctree:: :maxdepth: 1 - joining-a-network - corda-test-networks + compatibility-zones + corda-testnet-intro running-a-notary permissioning network-map diff --git a/docs/source/corda-test-networks.rst b/docs/source/corda-test-networks.rst deleted file mode 100644 index f5c2991fcf..0000000000 --- a/docs/source/corda-test-networks.rst +++ /dev/null @@ -1,77 +0,0 @@ -.. _log4j2: http://logging.apache.org/log4j/2.x/ - -Corda networks -============== - -A Corda network consists of a number of machines running nodes. These nodes communicate using persistent protocols in -order to create and validate transactions. - -There are three broader categories of functionality one such node may have. These pieces of functionality are provided -as services, and one node may run several of them. - -* Notary: Nodes running a notary service witness state spends and have the final say in whether a transaction is a - double-spend or not -* Oracle: Network services that link the ledger to the outside world by providing facts that affect the validity of - transactions -* Regular node: All nodes have a vault and may start protocols communicating with other nodes, notaries and oracles and - evolve their private ledger - -Bootstrap your own test network -------------------------------- - -Certificates -~~~~~~~~~~~~ - -Every node in a given Corda network must have an identity certificate signed by the network's root CA. See -:doc:`permissioning` for more information. - -Configuration -~~~~~~~~~~~~~ - -A node can be configured by adding/editing ``node.conf`` in the node's directory. For details see :doc:`corda-configuration-file`. - -An example configuration: - -.. literalinclude:: example-code/src/main/resources/example-node.conf -:language: cfg - - The most important fields regarding network configuration are: - - * ``p2pAddress``: This specifies a host and port to which Artemis will bind for messaging with other nodes. Note that the - address bound will **NOT** be ``my-corda-node``, but rather ``::`` (all addresses on all network interfaces). The hostname specified - is the hostname *that must be externally resolvable by other nodes in the network*. In the above configuration this is the - resolvable name of a machine in a VPN. -* ``rpcAddress``: The address to which Artemis will bind for RPC calls. -* ``webAddress``: The address the webserver should bind. Note that the port must be distinct from that of ``p2pAddress`` and ``rpcAddress`` if they are on the same machine. - -Starting the nodes -~~~~~~~~~~~~~~~~~~ - -You will first need to create the local network by bootstrapping it with the bootstrapper. Details of how to do that -can be found in :doc:`network-bootstrapper`. - -Once that's done you may now start the nodes in any order. You should see a banner, some log lines and eventually -``Node started up and registered``, indicating that the node is fully started. - -.. TODO: Add a better way of polling for startup. A programmatic way of determining whether a node is up is to check whether it's ``webAddress`` is bound. - -In terms of process management there is no prescribed method. You may start the jars by hand or perhaps use systemd and friends. - -Logging -~~~~~~~ - -Only a handful of important lines are printed to the console. For -details/diagnosing problems check the logs. - -Logging is standard log4j2_ and may be configured accordingly. Logs -are by default redirected to files in ``NODE_DIRECTORY/logs/``. - -Connecting to the nodes -~~~~~~~~~~~~~~~~~~~~~~~ - -Once a node has started up successfully you may connect to it as a client to initiate protocols/query state etc. -Depending on your network setup you may need to tunnel to do this remotely. - -See the :doc:`tutorial-clientrpc-api` on how to establish an RPC link. - -Sidenote: A client is always associated with a single node with a single identity, which only sees their part of the ledger. diff --git a/docs/source/corda-testnet-intro.rst b/docs/source/corda-testnet-intro.rst index 908eca6939..370a710320 100644 --- a/docs/source/corda-testnet-intro.rst +++ b/docs/source/corda-testnet-intro.rst @@ -22,7 +22,7 @@ Click on "Join the Corda Testnet". Select whether you want to register a company or as an individual on the Testnet. -This will create you an account with the Testnet onboarding application which will enable you to provision and manage multiple Corda nodes on Testnet. You will log in to this account to view and manage you Corda Testnet identitiy certificates. +This will create an account with the Testnet on-boarding application which will enable you to provision and manage multiple Corda nodes on Testnet. You will log in to this account to view and manage you Corda Testnet identity certificates. .. image:: resources/testnet-account-type.png @@ -30,23 +30,17 @@ Fill in the form with your details. .. note:: - Testnet is currently invitation only. If your request is approved you will receive an email. Please fill in as many details as possible as it helps us prioritise. The approval process will take place daily by a member of the R3 operations team reviewing all invite requests and making a decision based on the current rate of onboarding of new customers. + Testnet is currently invitation only. If your request is approved you will receive an email. Please fill in as many details as possible as it helps us prioritise requests. The approval process will take place daily by a member of the r3 operations team reviewing all invite requests and making a decision based on current rate of onboarding of new customers. .. image:: resources/testnet-form.png -.. note:: - - We currently only support federated login using Google email accounts. Please ensure the email you use to register is a Gmail account or is set up as a Google account and that you use this email to log in. - - Gmail is recommended. If you want to use a non-Gmail account you can enable your email for Google: https://support.google.com/accounts/answer/176347?hl=en - Once you have been approved, navigate to https://testnet.corda.network and click on "I have an invitation". -Sign in using the Google login service: +Sign in using either your email address and password, or "Sign in with Google": .. image:: resources/testnet-signin.png -When prompted approve the Testnet application: +If using Google accounts, approve the Testnet application when prompted: .. image:: resources/testnet-signin-auth.png @@ -62,11 +56,17 @@ You can now copy the ``ONE-TIME-KEY`` and paste it into the parameter form of yo .. image:: resources/testnet-platform-clean.png -Your node will register itself with the Corda Testnet when it first runs and be added to the global network map and be visible to counterparties after approximately 5 minutes. +Once your cloud instance is set up you can install and run your Testnet pre-provisioned Corda node by clicking on "Copy" and pasting the one time link into your remote cloud terminal. + +The installation script will download the Corda binaries as well as your PKI certificates, private keys and supporting files and will install and run Corda on your fresh cloud VM. Your node will register itself with the Corda Testnet when it first runs and be added to the global network map and be visible to counterparties after approximately 5 minutes. + +Hosting a Corda node locally is possible but will require manually configuring firewall and port forwarding on your local router. If you want this option then click on the "Download" button to download a Zip file with a pre-configured Corda node. + +.. note:: If you host your node on your own machine or a corporate server you must ensure it is reachable from the public internet at a specific IP address. Please follow the instructions here: :doc:`deploy-locally`. A note on identities on Corda Testnet ------------------------------------- -Unlike the main Corda Network, which is designed for verified real world identities, The Corda Testnet automatically assigns a "distinguished name" as your identity on the network. This is to prevent name abuse such as the use of offensive language in the names or name squatting. This allows the provisioning of a node to be automatic and instantaneous. It also enables the same user to safely generate many nodes without accidental name conflicts. If you require a human readable name then please contact support and a partial organsation name can be approved. +Unlike the main Corda Network, which is designed for verified real world identities, The Corda Testnet automatically assigns a "distinguished name" as your identity on the network. This is to prevent name abuse such as the use of offensive language in the names or name squatting. This allows the provision of a node to be automatic and instantaneous. It also enables the same user to safely generate many nodes without accidental name conflicts. If you require a human readable name then please contact support and a partial organisation name can be approved. diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperation.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperation.java index d313fdb8ce..7b23e22efe 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperation.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperation.java @@ -12,7 +12,7 @@ public final class SummingOperation implements FlowAsyncOperation { @NotNull @Override - public CordaFuture execute() { + public CordaFuture execute(String deduplicationId) { return CordaFutureImplKt.doneFuture(this.a + this.b); } diff --git a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperationThrowing.java b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperationThrowing.java index 1a759074b0..91c30eaf4c 100644 --- a/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperationThrowing.java +++ b/docs/source/example-code/src/main/java/net/corda/docs/java/tutorial/flowstatemachines/SummingOperationThrowing.java @@ -11,7 +11,7 @@ public final class SummingOperationThrowing implements FlowAsyncOperation execute() { + public CordaFuture execute(String deduplicationId) { throw new IllegalStateException("You shouldn't be calling me"); } diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt index dfbf8c158d..c956a713ec 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/kotlin/tutorial/flowstatemachines/TutorialFlowAsyncOperation.kt @@ -11,7 +11,7 @@ import net.corda.core.internal.executeAsync // DOCSTART SummingOperation class SummingOperation(val a: Int, val b: Int) : FlowAsyncOperation { - override fun execute(): CordaFuture { + override fun execute(deduplicationId: String): CordaFuture { return doneFuture(a + b) } } @@ -19,7 +19,7 @@ class SummingOperation(val a: Int, val b: Int) : FlowAsyncOperation { // DOCSTART SummingOperationThrowing class SummingOperationThrowing(val a: Int, val b: Int) : FlowAsyncOperation { - override fun execute(): CordaFuture { + override fun execute(deduplicationId: String): CordaFuture { throw IllegalStateException("You shouldn't be calling me") } } diff --git a/docs/source/resources/state-hierarchy.png b/docs/source/resources/state-hierarchy.png index a1c950683a..232ac7d1f5 100644 Binary files a/docs/source/resources/state-hierarchy.png and b/docs/source/resources/state-hierarchy.png differ diff --git a/docs/source/running-a-notary.rst b/docs/source/running-a-notary.rst index 04d797814c..7984a470db 100644 --- a/docs/source/running-a-notary.rst +++ b/docs/source/running-a-notary.rst @@ -43,6 +43,8 @@ Byzantine fault-tolerant (experimental) A prototype BFT notary implementation based on `BFT-Smart `_ is available. You can try it out on our `notary demo `_ page. Note that it -is still experimental and there is active work ongoing for a production ready solution. +is still experimental and there is active work ongoing for a production ready solution. Additionally, BFT-Smart requires Java +serialization which is disabled by default in Corda due to security risks, and it will only work in dev mode where this can +be customised. We do not recommend using it in any long-running test or production deployments. diff --git a/experimental/notary-bft-smart/build.gradle b/experimental/notary-bft-smart/build.gradle index 6987007fe4..110c569d14 100644 --- a/experimental/notary-bft-smart/build.gradle +++ b/experimental/notary-bft-smart/build.gradle @@ -3,6 +3,7 @@ apply plugin: 'kotlin-jpa' apply plugin: 'idea' apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' configurations { integrationTestCompile.extendsFrom testCompile diff --git a/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BFTSMaRtConfig.kt b/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BFTSMaRtConfig.kt index 489f190d6f..d31e6fb203 100644 --- a/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BFTSMaRtConfig.kt +++ b/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BFTSMaRtConfig.kt @@ -92,10 +92,3 @@ fun maxFaultyReplicas(clusterSize: Int) = (clusterSize - 1) / 3 fun minCorrectReplicas(clusterSize: Int) = (2 * clusterSize + 3) / 3 fun minClusterSize(maxFaultyReplicas: Int) = maxFaultyReplicas * 3 + 1 -fun bftSMaRtSerialFilter(clazz: Class<*>): Boolean = clazz.name.let { - it.startsWith("bftsmart.") - || it.startsWith("java.security.") - || it.startsWith("java.util.") - || it.startsWith("java.lang.") - || it.startsWith("java.net.") -} diff --git a/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BftSmartNotaryService.kt b/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BftSmartNotaryService.kt index 57560699cb..391b92a630 100644 --- a/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BftSmartNotaryService.kt +++ b/experimental/notary-bft-smart/src/main/kotlin/net/corda/notary/bftsmart/BftSmartNotaryService.kt @@ -10,7 +10,6 @@ import net.corda.core.identity.Party import net.corda.core.internal.notary.NotaryInternalException import net.corda.core.internal.notary.NotaryService import net.corda.core.internal.notary.verifySignature -import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentStateRef import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize @@ -41,6 +40,17 @@ class BftSmartNotaryService( ) : NotaryService() { companion object { private val log = contextLogger() + @JvmStatic + val serializationFilter + get() = { clazz: Class<*> -> + clazz.name.let { + it.startsWith("bftsmart.") + || it.startsWith("java.security.") + || it.startsWith("java.util.") + || it.startsWith("java.lang.") + || it.startsWith("java.net.") + } + } } private val notaryConfig = services.configuration.notary diff --git a/experimental/notary-raft/build.gradle b/experimental/notary-raft/build.gradle index c9187c450d..005abcd63d 100644 --- a/experimental/notary-raft/build.gradle +++ b/experimental/notary-raft/build.gradle @@ -2,6 +2,7 @@ apply plugin: 'kotlin' apply plugin: 'idea' apply plugin: 'net.corda.plugins.cordapp' apply plugin: 'net.corda.plugins.publish-utils' +apply plugin: 'com.jfrog.artifactory' configurations { integrationTestCompile.extendsFrom testCompile diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt b/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt index 466b99be86..68f608d0aa 100644 --- a/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/flows/FlowRetryTest.kt @@ -3,9 +3,13 @@ package net.corda.node.flows import co.paralleluniverse.fibers.Suspendable import net.corda.client.rpc.CordaRPCClient import net.corda.core.CordaRuntimeException +import net.corda.core.concurrent.CordaFuture import net.corda.core.flows.* import net.corda.core.identity.Party +import net.corda.core.internal.FlowAsyncOperation import net.corda.core.internal.IdempotentFlow +import net.corda.core.internal.concurrent.doneFuture +import net.corda.core.internal.executeAsync import net.corda.core.messaging.startFlow import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.ProgressTracker @@ -67,6 +71,21 @@ class FlowRetryTest : IntegrationTest() { assertEquals("$numSessions:$numIterations", result) } + @Test + fun `async operation deduplication id is stable accross retries`() { + val user = User("mark", "dadada", setOf(Permissions.startFlow())) + driver(DriverParameters( + startNodesInProcess = isQuasarAgentSpecified(), + notarySpecs = emptyList() + )) { + val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow() + + CordaRPCClient(nodeAHandle.rpcAddress).start(user.username, user.password).use { + it.proxy.startFlow(::AsyncRetryFlow).returnValue.getOrThrow() + } + } + } + @Test fun `flow gives up after number of exceptions, even if this is the first line of the flow`() { val user = User("mark", "dadada", setOf(Permissions.startFlow())) @@ -229,6 +248,36 @@ class RetryFlow() : FlowLogic(), IdempotentFlow { } } +@StartableByRPC +class AsyncRetryFlow() : FlowLogic(), IdempotentFlow { + companion object { + object FIRST_STEP : ProgressTracker.Step("Step one") + + fun tracker() = ProgressTracker(FIRST_STEP) + + val deduplicationIds = mutableSetOf() + } + + class RecordDeduplicationId: FlowAsyncOperation { + override fun execute(deduplicationId: String): CordaFuture { + val dedupeIdIsNew = deduplicationIds.add(deduplicationId) + if (dedupeIdIsNew) { + throw ExceptionToCauseFiniteRetry() + } + return doneFuture(deduplicationId) + } + } + + override val progressTracker = tracker() + + @Suspendable + override fun call(): String { + progressTracker.currentStep = FIRST_STEP + executeAsync(RecordDeduplicationId()) + return "Result" + } +} + @StartableByRPC class ThrowingFlow() : FlowLogic(), IdempotentFlow { companion object { @@ -248,4 +297,4 @@ class ThrowingFlow() : FlowLogic(), IdempotentFlow { progressTracker.currentStep = FIRST_STEP return "Result" } -} \ No newline at end of file +} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt index 59db4090f8..4fe4b47650 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/network/NetworkMapTest.kt @@ -57,18 +57,22 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP @JvmStatic @Parameterized.Parameters(name = "{0}") fun runParams() = listOf( - { addr: URL, nms: NetworkMapServer -> - SharedCompatibilityZoneParams( + { + addr: URL, + nms: NetworkMapServer -> SharedCompatibilityZoneParams( addr, + pnm = null, publishNotaries = { nms.networkParameters = testNetworkParameters(it, modifiedTime = Instant.ofEpochMilli(random63BitValue()), epoch = 2) } ) }, - { addr: URL, nms: NetworkMapServer -> - SplitCompatibilityZoneParams( + { + addr: URL, + nms: NetworkMapServer -> SplitCompatibilityZoneParams ( doormanURL = URL("http://I/Don't/Exist"), networkMapURL = addr, + pnm = null, publishNotaries = { nms.networkParameters = testNetworkParameters(it, modifiedTime = Instant.ofEpochMilli(random63BitValue()), epoch = 2) } diff --git a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt index 3a443d0954..2a10fa56c5 100644 --- a/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/utilities/registration/NodeRegistrationTest.kt @@ -84,6 +84,7 @@ class NodeRegistrationTest : IntegrationTest() { fun `node registration correct root cert`() { val compatibilityZone = SharedCompatibilityZoneParams( URL("http://$serverHostAndPort"), + null, publishNotaries = { server.networkParameters = testNetworkParameters(it) }, rootCert = DEV_ROOT_CA.certificate) internalDriver( diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 43f0ad18f9..1709f7b4c9 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -33,6 +33,7 @@ import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.* import net.corda.node.CordaClock +import net.corda.node.SerialFilter import net.corda.node.VersionInfo import net.corda.node.cordapp.CordappLoader import net.corda.node.internal.classloading.requireAnnotation @@ -87,6 +88,7 @@ import org.slf4j.Logger import rx.Observable import rx.Scheduler import java.io.IOException +import java.lang.UnsupportedOperationException import java.lang.management.ManagementFactory import java.lang.reflect.InvocationTargetException import java.nio.file.Paths @@ -155,6 +157,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, identityService::wellKnownPartyFromAnonymous, schemaService, cacheFactory) + init { // TODO Break cyclic dependency identityService.database = database @@ -796,6 +799,10 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val notaryKey = myNotaryIdentity?.owningKey ?: throw IllegalArgumentException("Unable to start notary service $serviceClass: notary identity not found") + + /** Some notary implementations only work with Java serialization. */ + maybeInstallSerializationFilter(serviceClass) + val constructor = serviceClass.getDeclaredConstructor(ServiceHubInternal::class.java, PublicKey::class.java).apply { isAccessible = true } val service = constructor.newInstance(services, notaryKey) as NotaryService @@ -809,6 +816,23 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } } + /** Installs a custom serialization filter defined by a notary service implementation. Only supported in dev mode. */ + private fun maybeInstallSerializationFilter(serviceClass: Class) { + try { + @Suppress("UNCHECKED_CAST") + val filter = serviceClass.getDeclaredMethod("getSerializationFilter").invoke(null) as ((Class<*>) -> Boolean) + if (configuration.devMode) { + log.warn("Installing a custom Java serialization filter, required by ${serviceClass.name}. " + + "Note this is only supported in dev mode – a production node will fail to start if serialization filters are used.") + SerialFilter.install(filter) + } else { + throw UnsupportedOperationException("Unable to install a custom Java serialization filter, not in dev mode.") + } + } catch (e: NoSuchMethodException) { + // No custom serialization filter declared + } + } + private fun getNotaryServiceClass(className: String): Class { val loadedImplementations = cordappLoader.cordapps.mapNotNull { it.notaryService } log.debug("Notary service implementations found: ${loadedImplementations.joinToString(", ")}") diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index caf89a6f69..42914cbb8e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -7,7 +7,6 @@ import io.netty.channel.unix.Errors import net.corda.cliutils.CordaCliWrapper import net.corda.cliutils.CordaVersionProvider import net.corda.cliutils.ExitCodes -import net.corda.core.cordapp.Cordapp import net.corda.core.crypto.Crypto import net.corda.core.internal.* import net.corda.core.internal.concurrent.thenMatch @@ -155,7 +154,7 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { private val handleRegistrationError = { error: Exception -> when (error) { - is NodeRegistrationException -> error.logAsExpected("Node registration service is unavailable. Perhaps try to perform the initial registration again after a while.") + is NodeRegistrationException -> error.logAsExpected("Issue with Node registration: ${error.message}") else -> error.logAsUnexpected("Exception during node registration") } } @@ -388,17 +387,23 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { logger.info(nodeStartedMessage) } - protected open fun registerWithNetwork(conf: NodeConfiguration, versionInfo: VersionInfo, nodeRegistrationConfig: NodeRegistrationOption) { - val compatibilityZoneURL = conf.networkServices?.doormanURL ?: throw RuntimeException( - "compatibilityZoneURL or networkServices must be configured!") + protected open fun registerWithNetwork( + conf: NodeConfiguration, + versionInfo: VersionInfo, + nodeRegistrationConfig: NodeRegistrationOption + ) { + println("\n" + + "******************************************************************\n" + + "* *\n" + + "* Registering as a new participant with a Corda network *\n" + + "* *\n" + + "******************************************************************\n") - println() - println("******************************************************************") - println("* *") - println("* Registering as a new participant with Corda network *") - println("* *") - println("******************************************************************") - NodeRegistrationHelper(conf, HTTPNetworkRegistrationService(compatibilityZoneURL, versionInfo), nodeRegistrationConfig).buildKeystore() + NodeRegistrationHelper(conf, + HTTPNetworkRegistrationService( + requireNotNull(conf.networkServices), + versionInfo), + nodeRegistrationConfig).buildKeystore() // Minimal changes to make registration tool create node identity. // TODO: Move node identity generation logic from node to registration helper. @@ -411,29 +416,16 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { protected open fun loadConfigFile(): Pair> = cmdLineOptions.loadConfig() protected open fun banJavaSerialisation(conf: NodeConfiguration) { + // Enterprise only - Oracle database requires additional serialization val isOracleDbDriver = conf.dataSourceProperties.getProperty("dataSource.url", "").startsWith("jdbc:oracle:") - val filter = - if (conf.notary?.bftSMaRt != null && isOracleDbDriver) { - val bftAndOracleSerialFilter: (Class<*>) -> Boolean = { clazz -> bftSMaRtSerialFilter(clazz) || oracleJdbcDriverSerialFilter(clazz) } - bftAndOracleSerialFilter - } else if (conf.notary?.bftSMaRt != null) { - ::bftSMaRtSerialFilter - } else if (isOracleDbDriver) { + val serialFilter = + if (isOracleDbDriver) { ::oracleJdbcDriverSerialFilter } else { ::defaultSerialFilter } - SerialFilter.install(filter) - } - - /** This filter is required for BFT-Smart to work as it only supports Java serialization. */ - // TODO: move this filter out of the node, allow Cordapps to specify filters. - private fun bftSMaRtSerialFilter(clazz: Class<*>): Boolean = clazz.name.let { - it.startsWith("bftsmart.") - || it.startsWith("java.security.") - || it.startsWith("java.util.") - || it.startsWith("java.lang.") - || it.startsWith("java.net.") + // Note that in dev mode this filter can be overridden by a notary service implementation. + SerialFilter.install(serialFilter) } protected open fun getVersionInfo(): VersionInfo { diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 4d35c5e07f..c605bdaa67 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -206,6 +206,8 @@ data class BFTSMaRtConfiguration( * * @property doormanURL The URL of the tls certificate signing service. * @property networkMapURL The URL of the Network Map service. + * @property pnm If the compatibility zone operator supports the private network map option, have the node + * at registration automatically join that private network. * @property inferred Non user setting that indicates weather the Network Services configuration was * set explicitly ([inferred] == false) or weather they have been inferred via the compatibilityZoneURL parameter * ([inferred] == true) where both the network map and doorman are running on the same endpoint. Only one, @@ -214,6 +216,7 @@ data class BFTSMaRtConfiguration( data class NetworkServicesConfig( val doormanURL: URL, val networkMapURL: URL, + val pnm: UUID? = null, val inferred : Boolean = false ) @@ -452,8 +455,9 @@ data class NodeConfigurationImpl( """.trimMargin()) } + // Support the deprecated method of configuring network services with a single compatibilityZoneURL option if (compatibilityZoneURL != null && networkServices == null) { - networkServices = NetworkServicesConfig(compatibilityZoneURL, compatibilityZoneURL, true) + networkServices = NetworkServicesConfig(compatibilityZoneURL, compatibilityZoneURL, inferred = true) } require(h2port == null || h2Settings == null) { "Cannot specify both 'h2port' and 'h2Settings' in configuration" } } diff --git a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt index 6a1fcb26e8..842af5589c 100644 --- a/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt +++ b/node/src/main/kotlin/net/corda/node/services/schema/NodeSchemaService.kt @@ -2,6 +2,7 @@ package net.corda.node.services.schema import net.corda.core.contracts.ContractState import net.corda.core.contracts.FungibleAsset +import net.corda.core.contracts.FungibleState import net.corda.core.contracts.LinearState import net.corda.core.schemas.* import net.corda.core.schemas.MappedSchemaValidator.crossReferencesToOtherMappedSchema @@ -66,6 +67,8 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() if (state is LinearState) schemas += VaultSchemaV1 // VaultLinearStates if (state is FungibleAsset<*>) + schemas += VaultSchemaV1 // VaultFungibleAssets + if (state is FungibleState<*>) schemas += VaultSchemaV1 // VaultFungibleStates return schemas @@ -77,6 +80,14 @@ class NodeSchemaService(private val extraSchemas: Set = emptySet() return VaultSchemaV1.VaultLinearStates(state.linearId, state.participants) if ((schema === VaultSchemaV1) && (state is FungibleAsset<*>)) return VaultSchemaV1.VaultFungibleStates(state.owner, state.amount.quantity, state.amount.token.issuer.party, state.amount.token.issuer.reference, state.participants) + if ((schema === VaultSchemaV1) && (state is FungibleState<*>)) + return VaultSchemaV1.VaultFungibleStates( + participants = state.participants.toMutableSet(), + owner = null, + quantity = state.amount.quantity, + issuer = null, + issuerRef = null + ) return (state as QueryableState).generateMappedObject(schema) } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt index 73ac04b73e..a785347ff3 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/Action.kt @@ -124,7 +124,7 @@ sealed class Action { /** * Execute the specified [operation]. */ - data class ExecuteAsyncOperation(val operation: FlowAsyncOperation<*>) : Action() + data class ExecuteAsyncOperation(val deduplicationId: String, val operation: FlowAsyncOperation<*>) : Action() /** * Release soft locks associated with given ID (currently the flow ID). diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt index 00a0406dbe..bd2e5a5169 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/ActionExecutorImpl.kt @@ -221,7 +221,7 @@ class ActionExecutorImpl( @Suspendable private fun executeAsyncOperation(fiber: FlowFiber, action: Action.ExecuteAsyncOperation) { - val operationFuture = action.operation.execute() + val operationFuture = action.operation.execute(action.deduplicationId) operationFuture.thenMatch( success = { result -> fiber.scheduleEvent(Event.AsyncOperationCompletion(result)) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt index 7dd43cb299..2d4a9b6ddc 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/transitions/StartedFlowTransition.kt @@ -411,7 +411,10 @@ class StartedFlowTransition( private fun executeAsyncOperation(flowIORequest: FlowIORequest.ExecuteAsyncOperation<*>): TransitionResult { return builder { - actions.add(Action.ExecuteAsyncOperation(flowIORequest.operation)) + // The `numberOfSuspends` is added to the deduplication ID in case an async + // operation is executed multiple times within the same flow. + val deduplicationId = context.id.toString() + ":" + currentState.checkpoint.numberOfSuspends.toString() + actions.add(Action.ExecuteAsyncOperation(deduplicationId, flowIORequest.operation)) FlowContinuation.ProcessEvents } } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 6f15c3466c..eed4a07b59 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -10,10 +10,12 @@ import net.corda.core.internal.* import net.corda.core.messaging.DataFeed import net.corda.core.node.ServicesForResolution import net.corda.core.node.StatesToRecord +import net.corda.core.node.services.* import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.StatesNotAvailableException import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault.ConstraintInfo.Companion.constraintInfo +import net.corda.core.node.services.vault.* import net.corda.core.node.services.VaultQueryException import net.corda.core.node.services.vault.* import net.corda.core.schemas.PersistentStateRef @@ -331,6 +333,17 @@ class NodeVaultService( // flowId was required by SoftLockManager to perform auto-registration of soft locks for new states val uuid = (Strand.currentStrand() as? FlowStateMachineImpl<*>)?.id?.uuid val vaultUpdate = if (uuid != null) netUpdate.copy(flowId = uuid) else netUpdate + if (uuid != null) { + val fungible = netUpdate.produced.filter { stateAndRef -> + val state = stateAndRef.state.data + state is FungibleAsset<*> || state is FungibleState<*> + } + if (fungible.isNotEmpty()) { + val stateRefs = fungible.map { it.ref }.toNonEmptySet() + log.trace { "Reserving soft locks for flow id $uuid and states $stateRefs" } + softLockReserve(uuid, stateRefs) + } + } persistentStateService.persist(vaultUpdate.produced) updatesPublisher.onNext(vaultUpdate) } @@ -443,14 +456,27 @@ class NodeVaultService( @Suspendable @Throws(StatesNotAvailableException::class) - override fun , U : Any> tryLockFungibleStatesForSpending(lockId: UUID, - eligibleStatesQuery: QueryCriteria, - amount: Amount, - contractStateType: Class): List> { + override fun > tryLockFungibleStatesForSpending( + lockId: UUID, + eligibleStatesQuery: QueryCriteria, + amount: Amount<*>, + contractStateType: Class + ): List> { if (amount.quantity == 0L) { return emptyList() } + // Helper to unwrap the token from the Issued object if one exists. + fun unwrapIssuedAmount(amount: Amount<*>): Any { + val token = amount.token + return when (token) { + is Issued<*> -> token.product + else -> token + } + } + + val unwrappedToken = unwrapIssuedAmount(amount) + // Enrich QueryCriteria with additional default attributes (such as soft locks). // We only want to return RELEVANT states here. val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF) @@ -465,8 +491,10 @@ class NodeVaultService( var claimedAmount = 0L val claimedStates = mutableListOf>() for (state in results.states) { - val issuedAssetToken = state.state.data.amount.token - if (issuedAssetToken.product == amount.token) { + // This method handles Amount> in FungibleAsset and Amount in FungibleState. + val issuedAssetToken = unwrapIssuedAmount(state.state.data.amount) + + if (issuedAssetToken == unwrappedToken) { claimedStates += state claimedAmount += state.state.data.amount.quantity if (claimedAmount > amount.quantity) { @@ -514,6 +542,7 @@ class NodeVaultService( val criteriaQuery = criteriaBuilder.createQuery(Tuple::class.java) val queryRootVaultStates = criteriaQuery.from(VaultSchemaV1.VaultStates::class.java) + // TODO: revisit (use single instance of parser for all queries) val criteriaParser = HibernateQueryCriteriaParser(contractStateType, contractStateTypeMappings, criteriaBuilder, criteriaQuery, queryRootVaultStates) @@ -580,6 +609,7 @@ class NodeVaultService( Vault.Page(states = statesAndRefs, statesMetadata = statesMeta, stateTypes = criteriaParser.stateTypes, totalStatesAvailable = totalStates, otherResults = otherResults) } } + @Throws(VaultQueryException::class) override fun _trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort, contractStateType: Class): DataFeed, Vault.Update> { return concurrentBox.exclusive { @@ -649,4 +679,4 @@ class NodeVaultService( } return myTypes } -} +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index 8755eccd94..db23815db2 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -145,9 +145,9 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio @Column(name = "issuer_name", nullable = true) var issuer: AbstractParty?, - @Column(name = "issuer_ref", length = MAX_ISSUER_REF_SIZE, nullable = false) + @Column(name = "issuer_ref", length = MAX_ISSUER_REF_SIZE, nullable = true) @Type(type = "corda-wrapper-binary") - var issuerRef: ByteArray + var issuerRef: ByteArray? ) : PersistentState() { constructor(_owner: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: OpaqueBytes, _participants: List) : this(owner = _owner, diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt index 3e422bc969..5de12f20c4 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/HTTPNetworkRegistrationService.kt @@ -6,6 +6,7 @@ import net.corda.core.internal.post import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.seconds import net.corda.node.VersionInfo +import net.corda.node.services.config.NetworkServicesConfig import net.corda.nodeapi.internal.crypto.X509CertificateFactory import okhttp3.CacheControl import okhttp3.Headers @@ -19,8 +20,11 @@ import java.util.* import java.util.zip.ZipInputStream import javax.naming.ServiceUnavailableException -class HTTPNetworkRegistrationService(compatibilityZoneURL: URL, val versionInfo: VersionInfo) : NetworkRegistrationService { - private val registrationURL = URL("$compatibilityZoneURL/certificate") +class HTTPNetworkRegistrationService( + val config : NetworkServicesConfig, + val versionInfo: VersionInfo +) : NetworkRegistrationService { + private val registrationURL = URL("${config.doormanURL}/certificate") companion object { private val TRANSIENT_ERROR_STATUS_CODES = setOf(HTTP_BAD_GATEWAY, HTTP_UNAVAILABLE, HTTP_GATEWAY_TIMEOUT) @@ -54,7 +58,8 @@ class HTTPNetworkRegistrationService(compatibilityZoneURL: URL, val versionInfo: override fun submitRequest(request: PKCS10CertificationRequest): String { return String(registrationURL.post(OpaqueBytes(request.encoded), "Platform-Version" to "${versionInfo.platformVersion}", - "Client-Version" to versionInfo.releaseVersion)) + "Client-Version" to versionInfo.releaseVersion, + "Private-Network-Map" to (config.pnm?.toString() ?: ""))) } } diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index 3d4e735498..eb1aac885b 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -96,10 +96,9 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path, val requestId = try { submitOrResumeCertificateSigningRequest(keyPair) } catch (e: Exception) { - if (e is ConnectException || e is ServiceUnavailableException || e is IOException) { - throw NodeRegistrationException(e) - } - throw e + throw if (e is ConnectException || e is ServiceUnavailableException || e is IOException) { + NodeRegistrationException(e.message, e) + } else e } val certificates = try { @@ -200,7 +199,8 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path, if (idlePeriodDuration != null) { Thread.sleep(idlePeriodDuration.toMillis()) } else { - throw NodeRegistrationException(e) + throw NodeRegistrationException("Compatibility Zone registration service is currently unavailable, " + + "try again later!.", e) } } } @@ -249,10 +249,17 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path, protected open fun isTlsCrlIssuerCertRequired(): Boolean = false } -class NodeRegistrationException(cause: Throwable?) : IOException("Unable to contact node registration service", cause) +class NodeRegistrationException( + message: String?, + cause: Throwable? +) : IOException(message ?: "Unable to contact node registration service", cause) -class NodeRegistrationHelper(private val config: NodeConfiguration, certService: NetworkRegistrationService, regConfig: NodeRegistrationOption, computeNextIdleDoormanConnectionPollInterval: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1))) : - NetworkRegistrationHelper( +class NodeRegistrationHelper( + private val config: NodeConfiguration, + certService: NetworkRegistrationService, + regConfig: NodeRegistrationOption, + computeNextIdleDoormanConnectionPollInterval: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1)) +) : NetworkRegistrationHelper( config.certificatesDirectory, config.signingCertificateStore, config.myLegalName, diff --git a/node/src/main/resources/migration/vault-schema.changelog-master.xml b/node/src/main/resources/migration/vault-schema.changelog-master.xml index a993718e77..d1b1cd6f3d 100644 --- a/node/src/main/resources/migration/vault-schema.changelog-master.xml +++ b/node/src/main/resources/migration/vault-schema.changelog-master.xml @@ -10,5 +10,6 @@ + diff --git a/node/src/main/resources/migration/vault-schema.changelog-v7.xml b/node/src/main/resources/migration/vault-schema.changelog-v7.xml new file mode 100644 index 0000000000..5b85396391 --- /dev/null +++ b/node/src/main/resources/migration/vault-schema.changelog-v7.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index 6ac3f82c7e..04fd10546c 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -109,20 +109,13 @@ class NotaryChangeTests { // Check that all encumbrances have been propagated to the outputs val originalOutputs = issueTx.outputStates val newOutputs = notaryChangeTx.outputStates - assertTrue(originalOutputs.minus(newOutputs).isEmpty()) + assertTrue(originalOutputs.size == newOutputs.size && originalOutputs.containsAll(newOutputs)) - // Check that encumbrance links aren't broken after notary change - val encumbranceLink = HashMap() - issueTx.outputs.forEach { - val currentState = it.data - val encumbranceState = it.encumbrance?.let { issueTx.outputs[it].data } - encumbranceLink[currentState] = encumbranceState - } - notaryChangeTx.outputs.forEach { - val currentState = it.data - val encumbranceState = it.encumbrance?.let { notaryChangeTx.outputs[it].data } - assertEquals(encumbranceLink[currentState], encumbranceState) - } + // Check if encumbrance linking between states has not changed. + val originalLinkedStates = issueTx.outputs.asSequence().filter { it.encumbrance != null }.map { Pair(it.data, issueTx.outputs[it.encumbrance!!].data) }.toSet() + val notaryChangeLinkedStates = notaryChangeTx.outputs.asSequence().filter { it.encumbrance != null }.map { Pair(it.data, notaryChangeTx.outputs[it.encumbrance!!].data) }.toSet() + + assertTrue { originalLinkedStates.size == notaryChangeLinkedStates.size && originalLinkedStates.containsAll(notaryChangeLinkedStates) } } @Test @@ -172,10 +165,11 @@ class NotaryChangeTests { val stateB = DummyContract.SingleOwnerState(Random().nextInt(), owner.party) val stateC = DummyContract.SingleOwnerState(Random().nextInt(), owner.party) + // Ensure encumbrances form a cycle. val tx = TransactionBuilder(null).apply { addCommand(Command(DummyContract.Commands.Create(), owner.party.owningKey)) addOutputState(stateA, DummyContract.PROGRAM_ID, notaryIdentity, encumbrance = 2) // Encumbered by stateB - addOutputState(stateC, DummyContract.PROGRAM_ID, notaryIdentity) + addOutputState(stateC, DummyContract.PROGRAM_ID, notaryIdentity, encumbrance = 0) // Encumbered by stateA addOutputState(stateB, DummyContract.PROGRAM_ID, notaryIdentity, encumbrance = 1) // Encumbered by stateC } val stx = services.signInitialTransaction(tx) diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowAsyncOperationTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowAsyncOperationTests.kt index c26f04447e..21765af86b 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowAsyncOperationTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowAsyncOperationTests.kt @@ -52,7 +52,7 @@ class FlowAsyncOperationTests { } private class ErroredExecute : FlowAsyncOperation { - override fun execute(): CordaFuture { + override fun execute(deduplicationId: String): CordaFuture { throw Exception() } } @@ -70,7 +70,7 @@ class FlowAsyncOperationTests { } private class ErroredResult : FlowAsyncOperation { - override fun execute(): CordaFuture { + override fun execute(deduplicationId: String): CordaFuture { val future = openFuture() future.setException(Exception()) return future @@ -103,7 +103,7 @@ class FlowAsyncOperationTests { } private class WorkerServiceTask(val completeAllTasks: Boolean, val service: WorkerService) : FlowAsyncOperation { - override fun execute(): CordaFuture { + override fun execute(deduplicationId: String): CordaFuture { return service.performTask(completeAllTasks) } } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 41a244990a..5bc7e6b287 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -129,6 +129,32 @@ class NodeVaultServiceTest { return tryLockFungibleStatesForSpending(lockId, baseCriteria, amount, Cash.State::class.java) } + @Test + fun `fungible state selection test`() { + val issuerParty = services.myInfo.legalIdentities.first() + class FungibleFoo(override val amount: Amount, override val participants: List) : FungibleState + val fungibleFoo = FungibleFoo(100.DOLLARS, listOf(issuerParty)) + services.apply { + val tx = signInitialTransaction(TransactionBuilder(DUMMY_NOTARY).apply { + addCommand(Command(DummyContract.Commands.Create(), issuerParty.owningKey)) + addOutputState(fungibleFoo, DummyContract.PROGRAM_ID) + }) + recordTransactions(listOf(tx)) + } + + val baseCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notary = listOf(DUMMY_NOTARY)) + + database.transaction { + val states = services.vaultService.tryLockFungibleStatesForSpending( + lockId = UUID.randomUUID(), + eligibleStatesQuery = baseCriteria, + amount = 10.DOLLARS, + contractStateType = FungibleFoo::class.java + ) + assertEquals(states.single().state.data.amount, 100.DOLLARS) + } + } + @Test fun `duplicate insert of transaction does not fail`() { database.transaction { diff --git a/notary/mysql/src/test/kotlin/net/corda/notary/mysql/MySQLNotaryServiceTests.kt b/notary/mysql/src/test/kotlin/net/corda/notary/mysql/MySQLNotaryServiceTests.kt index 39e2deaade..7a05f3e2d8 100644 --- a/notary/mysql/src/test/kotlin/net/corda/notary/mysql/MySQLNotaryServiceTests.kt +++ b/notary/mysql/src/test/kotlin/net/corda/notary/mysql/MySQLNotaryServiceTests.kt @@ -230,7 +230,7 @@ class MySQLNotaryServiceTests : IntegrationTest() { callerParty, requestSignature, null, - emptyList()).execute() + emptyList()).execute("") } return futures.transpose().get() } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt index 4604af4505..700a3b51bb 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/EvolutionSerializer.kt @@ -116,7 +116,6 @@ abstract class EvolutionSerializer( factory: SerializerFactory, constructor: KFunction, readersAsSerialized: Map): AMQPSerializer { - val constructorArgs = arrayOfNulls(constructor.parameters.size) // Java doesn't care about nullability unless it's a primitive in which // case it can't be referenced. Unfortunately whilst Kotlin does apply @@ -144,7 +143,7 @@ abstract class EvolutionSerializer( } } } - return EvolutionSerializerViaConstructor(new.type, factory, readersAsSerialized, constructor, constructorArgs) + return EvolutionSerializerViaConstructor(new.type, factory, readersAsSerialized, constructor) } private fun makeWithSetters( @@ -210,8 +209,7 @@ class EvolutionSerializerViaConstructor( clazz: Type, factory: SerializerFactory, oldReaders: Map, - kotlinConstructor: KFunction, - private val constructorArgs: Array) : EvolutionSerializer(clazz, factory, oldReaders, kotlinConstructor) { + kotlinConstructor: KFunction) : EvolutionSerializer(clazz, factory, oldReaders, kotlinConstructor) { /** * Unlike a normal [readObject] call where we simply apply the parameter deserialisers * to the object list of values we need to map that list, which is ordered per the @@ -226,6 +224,7 @@ class EvolutionSerializerViaConstructor( ): Any { if (obj !is List<*>) throw NotSerializableException("Body of described type is unexpected $obj") + val constructorArgs : Array = arrayOfNulls(kotlinConstructor.parameters.size) // *must* read all the parameters in the order they were serialized oldReaders.values.zip(obj).map { it.first.readProperty(it.second, schemas, input, constructorArgs, context) } diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt new file mode 100644 index 0000000000..3e4816b0c6 --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt @@ -0,0 +1,145 @@ +package net.corda.serialization.internal.model + +import com.google.common.reflect.TypeToken +import java.lang.reflect.* + +/** + * Used as a key for retrieving cached type information. We need slightly more information than the bare classname, + * and slightly less information than is captured by Java's [Type] (We drop type variance because in practice we resolve + * wildcards to their upper bounds, e.g. `? extends Foo` to `Foo`). We also need an identifier we can use even when the + * identified type is not visible from the current classloader. + * + * These identifiers act as the anchor for comparison between remote type information (prior to matching it to an actual + * local class) and local type information. + * + * [TypeIdentifier] provides a family of type identifiers, together with a [prettyPrint] method for displaying them. + */ +sealed class TypeIdentifier { + + /** + * The name of the type. + */ + abstract val name: String + + /** + * Obtain a nicely-formatted representation of the identified type, for help with debugging. + */ + fun prettyPrint(simplifyClassNames: Boolean = true): String = when(this) { + is TypeIdentifier.Unknown -> "?" + is TypeIdentifier.Top -> "*" + is TypeIdentifier.Unparameterised -> name.simplifyClassNameIfRequired(simplifyClassNames) + is TypeIdentifier.Erased -> "${name.simplifyClassNameIfRequired(simplifyClassNames)} (erased)" + is TypeIdentifier.ArrayOf -> "${componentType.prettyPrint()}[]" + is TypeIdentifier.Parameterised -> + name.simplifyClassNameIfRequired(simplifyClassNames) + parameters.joinToString(", ", "<", ">") { + it.prettyPrint() + } + } + + private fun String.simplifyClassNameIfRequired(simplifyClassNames: Boolean): String = + if (simplifyClassNames) split(".", "$").last() else this + + companion object { + /** + * Obtain the [TypeIdentifier] for an erased Java class. + * + * @param type The class to get a [TypeIdentifier] for. + */ + fun forClass(type: Class<*>): TypeIdentifier = when { + type.name == "java.lang.Object" -> Top + type.isArray -> ArrayOf(forClass(type.componentType)) + type.typeParameters.isEmpty() -> Unparameterised(type.name) + else -> Erased(type.name) + } + + /** + * Obtain the [TypeIdentifier] for a Java [Type] (typically obtained by calling one of + * [java.lang.reflect.Parameter.getAnnotatedType], + * [java.lang.reflect.Field.getGenericType] or + * [java.lang.reflect.Method.getGenericReturnType]). Wildcard types and type variables are converted to [Unknown]. + * + * @param type The [Type] to obtain a [TypeIdentifier] for. + * @param resolutionContext Optionally, a [Type] which can be used to resolve type variables, for example a + * class implementing a parameterised interface and specifying values for type variables which are referred to + * by methods defined in the interface. + */ + fun forGenericType(type: Type, resolutionContext: Type = type): TypeIdentifier = when(type) { + is ParameterizedType -> Parameterised((type.rawType as Class<*>).name, type.actualTypeArguments.map { + forGenericType(it.resolveAgainst(resolutionContext)) + }) + is Class<*> -> forClass(type) + is GenericArrayType -> ArrayOf(forGenericType(type.genericComponentType.resolveAgainst(resolutionContext))) + else -> Unknown + } + } + + /** + * The [TypeIdentifier] of [Any] / [java.lang.Object]. + */ + object Top : TypeIdentifier() { + override val name get() = "*" + override fun toString() = "Top" + } + + /** + * The [TypeIdentifier] of an unbounded wildcard. + */ + object Unknown : TypeIdentifier() { + override val name get() = "?" + override fun toString() = "Unknown" + } + + /** + * Identifies a class with no type parameters. + */ + data class Unparameterised(override val name: String) : TypeIdentifier() { + override fun toString() = "Unparameterised($name)" + } + + /** + * Identifies a parameterised class such as List, for which we cannot obtain the type parameters at runtime + * because they have been erased. + */ + data class Erased(override val name: String) : TypeIdentifier() { + override fun toString() = "Erased($name)" + } + + /** + * Identifies a type which is an array of some other type. + * + * @param componentType The [TypeIdentifier] of the component type of this array. + */ + data class ArrayOf(val componentType: TypeIdentifier) : TypeIdentifier() { + override val name get() = componentType.name + "[]" + override fun toString() = "ArrayOf(${componentType.prettyPrint()})" + } + + /** + * A parameterised class such as Map for which we have resolved type parameter values. + * + * @param parameters [TypeIdentifier]s for each of the resolved type parameter values of this type. + */ + data class Parameterised(override val name: String, val parameters: List) : TypeIdentifier() { + override fun toString() = "Parameterised(${prettyPrint()})" + } +} + +internal fun Type.resolveAgainst(context: Type): Type = when (this) { + is WildcardType -> this.upperBound + is ParameterizedType, + is TypeVariable<*> -> TypeToken.of(context).resolveType(this).type.upperBound + else -> this +} + +private val Type.upperBound: Type + get() = when (this) { + is TypeVariable<*> -> when { + this.bounds.isEmpty() || this.bounds.size > 1 -> this + else -> this.bounds[0] + } + is WildcardType -> when { + this.upperBounds.isEmpty() || this.upperBounds.size > 1 -> this + else -> this.upperBounds[0] + } + else -> this + } \ No newline at end of file diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/model/TypeIdentifierTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/model/TypeIdentifierTests.kt new file mode 100644 index 0000000000..f07f88526a --- /dev/null +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/model/TypeIdentifierTests.kt @@ -0,0 +1,53 @@ +package net.corda.serialization.internal.model + +import com.google.common.reflect.TypeToken +import net.corda.serialization.internal.model.TypeIdentifier.* +import org.junit.Test +import java.lang.reflect.Type +import kotlin.test.assertEquals + +class TypeIdentifierTests { + + @Test + fun `primitive types and arrays`() { + assertIdentified(Int::class.javaPrimitiveType!!, "int") + assertIdentified("Integer") + assertIdentified("int[]") + assertIdentified>("Integer[]") + } + + @Test + fun `erased and unerased`() { + assertIdentified(List::class.java, "List (erased)") + assertIdentified>("List") + } + + @Test + fun `nested parameterised`() { + assertIdentified>>("List>") + } + + interface HasArray { + val array: Array> + } + + class HasStringArray(override val array: Array>): HasArray + + @Test + fun `resolved against an owning type`() { + val fieldType = HasArray::class.java.getDeclaredMethod("getArray").genericReturnType + assertIdentified(fieldType, "List<*>[]") + + assertEquals( + "List[]", + TypeIdentifier.forGenericType(fieldType, HasStringArray::class.java).prettyPrint()) + } + + private fun assertIdentified(type: Type, expected: String) = + assertEquals(expected, TypeIdentifier.forGenericType(type).prettyPrint()) + + private inline fun assertIdentified(expected: String) = + assertEquals(expected, TypeIdentifier.forGenericType(typeOf()).prettyPrint()) + + private inline fun typeOf() = object : TypeToken() {}.type +} \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index bfef576b20..f125026ecc 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -245,7 +245,7 @@ class DriverDSLImpl( val registrationFuture = if (compatibilityZone?.rootCert != null) { // We don't need the network map to be available to be able to register the node - startNodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.doormanURL()) + startNodeRegistration(name, compatibilityZone.rootCert, compatibilityZone.config()) } else { doneFuture(Unit) } @@ -300,14 +300,18 @@ class DriverDSLImpl( return startNodeInternal(config, webAddress, startInSameProcess, maximumHeapSize, localNetworkMap, additionalCordapps, regenerateCordappsOnStart, bytemanPort) } - private fun startNodeRegistration(providedName: CordaX500Name, rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture { + private fun startNodeRegistration( + providedName: CordaX500Name, + rootCert: X509Certificate, + networkServicesConfig: NetworkServicesConfig + ): CordaFuture { val baseDirectory = baseDirectory(providedName).createDirectories() val config = NodeConfig(ConfigHelper.loadConfig( baseDirectory = baseDirectory, allowMissingConfig = true, configOverrides = configOf( "p2pAddress" to portAllocation.nextHostAndPort().toString(), - "compatibilityZoneURL" to compatibilityZoneURL.toString(), + "compatibilityZoneURL" to networkServicesConfig.doormanURL.toString(), "myLegalName" to providedName.toString(), "rpcSettings" to mapOf( "address" to portAllocation.nextHostAndPort().toString(), @@ -330,7 +334,7 @@ class DriverDSLImpl( executorService.fork { NodeRegistrationHelper( config.corda, - HTTPNetworkRegistrationService(compatibilityZoneURL, versionInfo), + HTTPNetworkRegistrationService(networkServicesConfig, versionInfo), NodeRegistrationOption(rootTruststorePath, rootTruststorePassword) ).buildKeystore() config @@ -396,7 +400,7 @@ class DriverDSLImpl( startNotaryIdentityGeneration() } else { // With a root cert specified we delegate generation of the notary identities to the CZ. - startAllNotaryRegistrations(compatibilityZone.rootCert, compatibilityZone.doormanURL()) + startAllNotaryRegistrations(compatibilityZone.rootCert, compatibilityZone) } notaryInfosFuture.map { notaryInfos -> compatibilityZone.publishNotaries(notaryInfos) @@ -447,16 +451,22 @@ class DriverDSLImpl( } } - private fun startAllNotaryRegistrations(rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture> { + private fun startAllNotaryRegistrations( + rootCert: X509Certificate, + compatibilityZone: CompatibilityZoneParams): CordaFuture> { // Start the registration process for all the notaries together then wait for their responses. return notarySpecs.map { spec -> require(spec.cluster == null) { "Registering distributed notaries not supported" } - startNotaryRegistration(spec, rootCert, compatibilityZoneURL) + startNotaryRegistration(spec, rootCert, compatibilityZone) }.transpose() } - private fun startNotaryRegistration(spec: NotarySpec, rootCert: X509Certificate, compatibilityZoneURL: URL): CordaFuture { - return startNodeRegistration(spec.name, rootCert, compatibilityZoneURL).flatMap { config -> + private fun startNotaryRegistration( + spec: NotarySpec, + rootCert: X509Certificate, + compatibilityZone: CompatibilityZoneParams + ): CordaFuture { + return startNodeRegistration(spec.name, rootCert, compatibilityZone.config()).flatMap { config -> // Node registration only gives us the node CA cert, not the identity cert. That is only created on first // startup or when the node is told to just generate its node info file. We do that here. if (startNodesInProcess) { @@ -1122,6 +1132,7 @@ sealed class CompatibilityZoneParams( ) { abstract fun networkMapURL(): URL abstract fun doormanURL(): URL + abstract fun config() : NetworkServicesConfig } /** @@ -1129,11 +1140,18 @@ sealed class CompatibilityZoneParams( */ class SharedCompatibilityZoneParams( private val url: URL, + private val pnm : UUID?, publishNotaries: (List) -> Unit, rootCert: X509Certificate? = null ) : CompatibilityZoneParams(publishNotaries, rootCert) { + + val config : NetworkServicesConfig by lazy { + NetworkServicesConfig(url, url, pnm, false) + } + override fun doormanURL() = url override fun networkMapURL() = url + override fun config() : NetworkServicesConfig = config } /** @@ -1142,11 +1160,17 @@ class SharedCompatibilityZoneParams( class SplitCompatibilityZoneParams( private val doormanURL: URL, private val networkMapURL: URL, + private val pnm : UUID?, publishNotaries: (List) -> Unit, rootCert: X509Certificate? = null ) : CompatibilityZoneParams(publishNotaries, rootCert) { + val config : NetworkServicesConfig by lazy { + NetworkServicesConfig(doormanURL, networkMapURL, pnm, false) + } + override fun doormanURL() = doormanURL override fun networkMapURL() = networkMapURL + override fun config() : NetworkServicesConfig = config } fun internalDriver( diff --git a/tools/notarytest/src/main/kotlin/net/corda/notarytest/flows/AsyncLoadTestFlow.kt b/tools/notarytest/src/main/kotlin/net/corda/notarytest/flows/AsyncLoadTestFlow.kt index 7a5a920ead..4a4a5155b4 100644 --- a/tools/notarytest/src/main/kotlin/net/corda/notarytest/flows/AsyncLoadTestFlow.kt +++ b/tools/notarytest/src/main/kotlin/net/corda/notarytest/flows/AsyncLoadTestFlow.kt @@ -73,7 +73,7 @@ open class AsyncLoadTestFlow( val requestSignature = NotarisationRequest(inputs, txId).generateSignature(serviceHub) futures += AsyncCFTNotaryService.CommitOperation(service, inputs, txId, callerParty, requestSignature, - null, emptyList()).execute() + null, emptyList()).execute("") } futures.transpose().get()