mirror of
https://github.com/corda/corda.git
synced 2025-03-15 08:41:04 +00:00
Merge pull request #1494 from corda/merges/os-merge-20181021
OS -> ENT merge 20181021
This commit is contained in:
commit
bc50e5f5b0
@ -564,7 +564,7 @@ public final class net.corda.core.contracts.ContractsDSL extends java.lang.Objec
|
||||
public static final java.util.List<net.corda.core.contracts.CommandWithParties<C>> select(java.util.Collection<? extends net.corda.core.contracts.CommandWithParties<? extends net.corda.core.contracts.CommandData>>, Class<C>, java.util.Collection<? extends java.security.PublicKey>, java.util.Collection<net.corda.core.identity.Party>)
|
||||
##
|
||||
@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<net.corda.core.contracts.Issued<T>> 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<T> withNewOwnerAndAmount(net.corda.core.contracts.Amount<net.corda.core.contracts.Issued<T>>, 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<T> 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$Page<T>, net.corda.core.node.services.Vault$Update<T>> trackBy(Class<? extends T>, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.node.services.vault.Sort)
|
||||
@Suspendable
|
||||
@NotNull
|
||||
public abstract java.util.List<net.corda.core.contracts.StateAndRef<T>> tryLockFungibleStatesForSpending(java.util.UUID, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.contracts.Amount<U>, Class<? extends T>)
|
||||
public abstract java.util.List<net.corda.core.contracts.StateAndRef<T>> tryLockFungibleStatesForSpending(java.util.UUID, net.corda.core.node.services.vault.QueryCriteria, net.corda.core.contracts.Amount<?>, Class<? extends T>)
|
||||
@NotNull
|
||||
public abstract net.corda.core.concurrent.CordaFuture<net.corda.core.node.services.Vault$Update<net.corda.core.contracts.ContractState>> whenConsumed(net.corda.core.contracts.StateRef)
|
||||
##
|
||||
|
4
.idea/compiler.xml
generated
4
.idea/compiler.xml
generated
@ -199,6 +199,8 @@
|
||||
<module name="loadtest_test" target="1.8" />
|
||||
<module name="mock_main" target="1.8" />
|
||||
<module name="mock_test" target="1.8" />
|
||||
<module name="net.corda-verifier_main" target="1.8" />
|
||||
<module name="net.corda-verifier_test" target="1.8" />
|
||||
<module name="mysql_main" target="1.8" />
|
||||
<module name="mysql_test" target="1.8" />
|
||||
<module name="net.corda_buildSrc_main" target="1.8" />
|
||||
@ -340,4 +342,4 @@
|
||||
<component name="JavacSettings">
|
||||
<option name="ADDITIONAL_OPTIONS_STRING" value="-parameters" />
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
|
@ -9,6 +9,7 @@ buildCache {
|
||||
enabled = !isCiServer
|
||||
}
|
||||
remote(HttpBuildCache) {
|
||||
enabled = isCiServer
|
||||
url = gradleBuildCacheURL
|
||||
push = isCiServer
|
||||
}
|
||||
|
@ -28,12 +28,12 @@ class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException
|
||||
* (GBP, USD, oil, shares in company <X>, etc.) and any additional metadata (issuer, grade, class, etc.).
|
||||
*/
|
||||
@KeepForDJVM
|
||||
interface FungibleAsset<T : Any> : OwnableState {
|
||||
interface FungibleAsset<T : Any> : FungibleState<Issued<T>>, 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<Issued<T>>
|
||||
override val amount: Amount<Issued<T>>
|
||||
|
||||
/**
|
||||
* There must be an ExitCommand signed by these keys to destroy the amount. While all states require their
|
||||
|
@ -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<Issued<T>>, 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 <X>, 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<T : Any> : 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<T>
|
||||
}
|
||||
// DOCEND 1
|
||||
|
@ -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<Int>)
|
||||
: 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
|
||||
}
|
||||
|
||||
|
@ -62,6 +62,7 @@ abstract class AbstractStateReplacementFlow {
|
||||
@Throws(StateReplacementException::class)
|
||||
override fun call(): StateAndRef<T> {
|
||||
val (stx) = assembleTx()
|
||||
stx.verify(serviceHub, checkSufficientSignatures = false)
|
||||
val participantSessions = getParticipantSessions()
|
||||
progressTracker.currentStep = SIGNING
|
||||
|
||||
|
@ -46,14 +46,14 @@ class NotaryChangeFlow<out T : ContractState>(
|
||||
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<T>): List<StateAndRef<T>> {
|
||||
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<T>(encumbranceStateRef)
|
||||
states.add(encumbranceState)
|
||||
if (!states.add(encumbranceState)) break // Stop if there is a cycle.
|
||||
}
|
||||
return states
|
||||
return states.toList()
|
||||
}
|
||||
}
|
||||
|
@ -12,8 +12,14 @@ import net.corda.core.serialization.CordaSerializable
|
||||
*/
|
||||
@CordaSerializable
|
||||
interface FlowAsyncOperation<R : Any> {
|
||||
/** Performs the operation in a non-blocking fashion. */
|
||||
fun execute(): CordaFuture<R>
|
||||
/**
|
||||
* 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<R>
|
||||
}
|
||||
// DOCEND FlowAsyncOperation
|
||||
|
||||
@ -24,4 +30,4 @@ fun <T, R : Any> FlowLogic<T>.executeAsync(operation: FlowAsyncOperation<R>, may
|
||||
val request = FlowIORequest.ExecuteAsyncOperation(operation)
|
||||
return stateMachine.suspend(request, maySkipCheckpoint)
|
||||
}
|
||||
// DOCEND executeAsync
|
||||
// DOCEND executeAsync
|
||||
|
@ -22,7 +22,7 @@ class WaitForStateConsumption(val stateRefs: Set<StateRef>, val services: Servic
|
||||
val logger = contextLogger()
|
||||
}
|
||||
|
||||
override fun execute(): CordaFuture<Unit> {
|
||||
override fun execute(deduplicationId: String): CordaFuture<Unit> {
|
||||
val futures = stateRefs.map { services.vaultService.whenConsumed(it).toCompletableFuture() }
|
||||
val completedFutures = futures.filter { it.isDone }
|
||||
|
||||
@ -40,4 +40,4 @@ class WaitForStateConsumption(val stateRefs: Set<StateRef>, val services: Servic
|
||||
|
||||
return CompletableFuture.allOf(*futures.toTypedArray()).thenApply { Unit }.asCordaFuture()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ abstract class AsyncCFTNotaryService : TrustedAuthorityNotaryService() {
|
||||
val timeWindow: TimeWindow?,
|
||||
val references: List<StateRef>
|
||||
): FlowAsyncOperation<Result> {
|
||||
override fun execute(): CordaFuture<Result> {
|
||||
override fun execute(deduplicationId: String): CordaFuture<Result> {
|
||||
return service.commitAsync(inputs, txId, caller, requestSignature, timeWindow, references)
|
||||
}
|
||||
}
|
||||
|
@ -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<Issued<*>> and Amount<*>. Amount<Issued<*>> 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 <T : FungibleAsset<U>, U : Any> tryLockFungibleStatesForSpending(lockId: UUID,
|
||||
eligibleStatesQuery: QueryCriteria,
|
||||
amount: Amount<U>,
|
||||
contractStateType: Class<out T>): List<StateAndRef<T>>
|
||||
fun <T : FungibleState<*>> tryLockFungibleStatesForSpending(lockId: UUID,
|
||||
eligibleStatesQuery: QueryCriteria,
|
||||
amount: Amount<*>,
|
||||
contractStateType: Class<out T>): List<StateAndRef<T>>
|
||||
|
||||
// DOCSTART VaultQueryAPI
|
||||
/**
|
||||
|
@ -168,6 +168,22 @@ sealed class QueryCriteria : GenericQueryCriteria<QueryCriteria, IQueryCriteriaP
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleStates]
|
||||
*/
|
||||
data class FungibleStateQueryCriteria(
|
||||
val participants: List<AbstractParty>? = null,
|
||||
val quantity: ColumnPredicate<Long>? = null,
|
||||
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
override val contractStateTypes: Set<Class<out ContractState>>? = null,
|
||||
override val relevancyStatus: Vault.RelevancyStatus = Vault.RelevancyStatus.ALL
|
||||
) : CommonQueryCriteria() {
|
||||
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
|
||||
super.visit(parser)
|
||||
return parser.parseCriteria(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleStates]
|
||||
*/
|
||||
|
@ -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<ContractState>, 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<Pair<Int, Int>>) {
|
||||
// [Set] of "from" (encumbered states).
|
||||
val encumberedSet = mutableSetOf<Int>()
|
||||
// [Set] of "to" (encumbrance states).
|
||||
val encumbranceSet = mutableSetOf<Int>()
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,13 +102,21 @@ data class NotaryChangeLedgerTransaction(
|
||||
|
||||
override val references: List<StateAndRef<ContractState>> = 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<TransactionState<ContractState>>
|
||||
get() = inputs.mapIndexed { pos, (state) ->
|
||||
get() = computeOutputs()
|
||||
|
||||
private fun computeOutputs(): List<TransactionState<ContractState>> {
|
||||
val inputPositionIndex: Map<StateRef, Int> = 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<PublicKey>
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -134,4 +134,3 @@ fun <V> Future<V>.getOrThrow(timeout: Duration? = null): V = try {
|
||||
} catch (e: ExecutionException) {
|
||||
throw e.cause!!
|
||||
}
|
||||
|
||||
|
@ -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<IdentityServiceInternal>().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<State>().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<IdentityServiceInternal>().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<TransactionVerificationException.TransactionNonMatchingEncumbranceException> {
|
||||
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<TransactionVerificationException.TransactionDuplicateEncumbranceException> {
|
||||
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<TransactionVerificationException.TransactionNonMatchingEncumbranceException> {
|
||||
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<TransactionVerificationException.TransactionNonMatchingEncumbranceException> {
|
||||
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 {
|
||||
|
@ -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/**'
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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<T> extends java.lang.Comparable<T> {
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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<E extends Enum<E>> extends Object implements Comparable<E>, Serializable {
|
||||
|
||||
private final String name;
|
||||
@ -24,4 +26,10 @@ public abstract class Enum<E extends Enum<E>> extends Object implements Comparab
|
||||
return ordinal;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
final java.lang.Enum<?> fromDJVM() {
|
||||
throw new UnsupportedOperationException("Dummy implementation");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<T> extends java.lang.Iterable<T> {
|
||||
@Override
|
||||
|
@ -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());
|
||||
}
|
||||
|
46
djvm/src/main/java/sandbox/java/lang/StackTraceElement.java
Normal file
46
djvm/src/main/java/sandbox/java/lang/StackTraceElement.java
Normal file
@ -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 + ')' : "")
|
||||
);
|
||||
}
|
||||
}
|
@ -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<String>, CharSequence, Serializable {
|
||||
@ -22,6 +24,18 @@ public final class String extends Object implements Comparable<String>, CharSequ
|
||||
private static final String TRUE = new String("true");
|
||||
private static final String FALSE = new String("false");
|
||||
|
||||
private static final Map<java.lang.String, String> 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<String>, 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<String>, CharSequ
|
||||
return toDJVM(value.trim());
|
||||
}
|
||||
|
||||
public String intern() { return INTERNAL.computeIfAbsent(value, s -> this); }
|
||||
|
||||
public char[] toCharArray() {
|
||||
return value.toCharArray();
|
||||
}
|
||||
|
@ -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 {
|
||||
|
||||
|
@ -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 {
|
||||
|
||||
|
137
djvm/src/main/java/sandbox/java/lang/Throwable.java
Normal file
137
djvm/src/main/java/sandbox/java/lang/Throwable.java
Normal file
@ -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("<init>");
|
||||
}
|
||||
|
||||
public void printStackTrace() {}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
java.lang.Throwable fromDJVM() {
|
||||
return DJVM.fromDJVM(this);
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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<T> extends java.util.Comparator<T> {
|
||||
|
@ -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();
|
||||
|
@ -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<T, R> {
|
||||
|
@ -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<T> {
|
||||
|
@ -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
|
||||
|
@ -58,11 +58,21 @@ class AnalysisConfiguration(
|
||||
*/
|
||||
val stitchedInterfaces: Map<String, List<Member>> get() = STITCHED_INTERFACES
|
||||
|
||||
/**
|
||||
* These classes have extra methods added as they are mapped into the sandbox.
|
||||
*/
|
||||
val stitchedClasses: Map<String, List<Member>> 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<String> = 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:
|
||||
*
|
||||
* <code>interface sandbox.A extends A</code>
|
||||
*/
|
||||
private val STITCHED_INTERFACES: Map<String, List<Member>> = 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<String, List<Member>> = 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<String, List<Member>> = 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<Class<*>>.sandboxed(): Set<String> = map(Companion::sandboxed).toSet()
|
||||
private fun Iterable<Member>.mapByClassName(): Map<String, List<Member>>
|
||||
= groupBy(Member::className).mapValues(Map.Entry<String, List<Member>>::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<MethodBody>()
|
||||
|
||||
protected open fun writeBody(emitter: EmitterModule) {}
|
||||
@ -179,7 +294,7 @@ class AnalysisConfiguration(
|
||||
className = className,
|
||||
memberName = memberName,
|
||||
signature = descriptor,
|
||||
genericsDetails = "",
|
||||
genericsDetails = signature,
|
||||
body = bodies
|
||||
)
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,41 @@
|
||||
package net.corda.djvm.analysis
|
||||
|
||||
import org.objectweb.asm.Type
|
||||
|
||||
class ExceptionResolver(
|
||||
private val jvmExceptionClasses: Set<String>,
|
||||
private val pinnedClasses: Set<String>,
|
||||
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
|
||||
}
|
||||
}
|
@ -35,4 +35,4 @@ class PrefixTree {
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -41,7 +41,11 @@ class ClassMutator(
|
||||
}
|
||||
}
|
||||
|
||||
private val emitters: List<Emitter> = 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<Emitter> = (emitters + PrependClassInitializer()).sortedBy(Emitter::priority)
|
||||
private val initializers = mutableListOf<MethodBody>()
|
||||
|
||||
/**
|
||||
@ -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<Emitter>(emitters.sortedByDescending(Emitter::isTracer), analysisContext.messages) {
|
||||
Processor.processEntriesOfType<Emitter>(emitters, analysisContext.messages) {
|
||||
it.emit(context, instruction)
|
||||
}
|
||||
if (!emitter.emitDefaultInstruction || emitter.hasEmittedCustomCode) {
|
||||
|
@ -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
|
||||
|
||||
}
|
@ -168,6 +168,14 @@ class EmitterModule(
|
||||
|
||||
inline fun <reified T : Throwable> 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.
|
||||
*/
|
||||
|
@ -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.
|
||||
|
@ -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()
|
@ -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)
|
||||
|
@ -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, "")
|
||||
|
@ -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<Member>()
|
||||
|
||||
override fun visit(version: Int, access: Int, className: String, signature: String?, superName: String?, interfaces: Array<String>?) {
|
||||
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<out String>?): 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<SandboxClassLoader>()
|
||||
private val UNMODIFIED = ByteCode(ByteArray(0), false)
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
*
|
||||
* <code>
|
||||
* 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;
|
||||
* }
|
||||
* }
|
||||
* </code>
|
||||
*/
|
||||
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, "<init>", CONSTRUCTOR_DESCRIPTOR, null, null).also { mv ->
|
||||
mv.visitCode()
|
||||
mv.visitVarInsn(ALOAD, 0)
|
||||
mv.visitMethodInsn(INVOKESPECIAL, superName, "<init>", "()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.
|
||||
*
|
||||
* <code>
|
||||
* public class CLASSNAME extends SUPERNAME {
|
||||
* public CLASSNAME(sandbox.java.lang.Throwable t) {
|
||||
* super(t);
|
||||
* }
|
||||
* }
|
||||
* </code>
|
||||
*/
|
||||
private fun ClassWriter.childClass() {
|
||||
// Class definition
|
||||
visit(
|
||||
V1_8,
|
||||
ACC_SYNTHETIC or ACC_PUBLIC,
|
||||
className,
|
||||
null,
|
||||
superName,
|
||||
arrayOf()
|
||||
)
|
||||
|
||||
// Constructor
|
||||
visitMethod(ACC_PUBLIC, "<init>", CONSTRUCTOR_DESCRIPTOR, null, null).also { mv ->
|
||||
mv.visitCode()
|
||||
mv.visitVarInsn(ALOAD, 0)
|
||||
mv.visitVarInsn(ALOAD, 1)
|
||||
mv.visitMethodInsn(INVOKESPECIAL, superName, "<init>", CONSTRUCTOR_DESCRIPTOR, false)
|
||||
mv.visitInsn(RETURN)
|
||||
mv.visitMaxs(2, 2)
|
||||
mv.visitEnd()
|
||||
}
|
||||
|
||||
// End of class
|
||||
visitEnd()
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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<Label, String>()
|
||||
|
||||
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
|
||||
}
|
@ -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;")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,10 @@ import sandbox.java.lang.unsandbox
|
||||
|
||||
typealias SandboxFunction<TInput, TOutput> = sandbox.java.util.function.Function<TInput, TOutput>
|
||||
|
||||
@Suppress("unused")
|
||||
internal fun isEntryPoint(elt: java.lang.StackTraceElement): Boolean {
|
||||
return elt.className == "sandbox.Task" && elt.methodName == "apply"
|
||||
}
|
||||
|
||||
class Task(private val function: SandboxFunction<in Any?, out Any?>?) : SandboxFunction<Any?, Any?> {
|
||||
|
||||
/**
|
||||
|
@ -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<Object>()
|
||||
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 <reified T : Object> Array<*>.toDJVMArray(): Array<out T?> {
|
||||
}
|
||||
}
|
||||
|
||||
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<Enum<*>>) as Array<Enum<*>>)[ordinal]
|
||||
@ -87,10 +99,11 @@ fun getEnumConstants(clazz: Class<out Enum<*>>): Array<*>? {
|
||||
|
||||
internal fun enumConstantDirectory(clazz: Class<out Enum<*>>): sandbox.java.util.Map<String, out Enum<*>>? {
|
||||
// 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<out Enum<*>>): Array<out Enum<*>>? {
|
||||
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<out Enum<*>>): Array<out Enum<*
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unchecked_cast")
|
||||
@Suppress("unchecked_cast", "ReplacePutWithAssignment" )
|
||||
private fun createEnum(clazz: Class<out Enum<*>>): Array<out Enum<*>>? {
|
||||
return clazz.getMethod("values").let { method ->
|
||||
method.isAccessible = true
|
||||
@ -109,6 +122,7 @@ private fun createEnum(clazz: Class<out Enum<*>>): Array<out Enum<*>>? {
|
||||
}?.apply { allEnums.put(clazz, this) }
|
||||
}
|
||||
|
||||
@Suppress("ReplacePutWithAssignment")
|
||||
private fun createEnumDirectory(clazz: Class<out Enum<*>>): sandbox.java.util.Map<String, out Enum<*>> {
|
||||
val universe = getEnumConstantsShared(clazz) ?: throw IllegalArgumentException("${clazz.name} is not an enum type")
|
||||
val directory = sandbox.java.util.LinkedHashMap<String, Enum<*>>(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<java.lang.StackTraceElement>): Array<StackTraceElement> {
|
||||
var idx = 0
|
||||
while (idx < source.size && !isEntryPoint(source[idx])) {
|
||||
++idx
|
||||
}
|
||||
return copyToDJVM(source, 0, idx)
|
||||
}
|
||||
|
||||
internal fun copyToDJVM(source: Array<java.lang.StackTraceElement>, fromIdx: Int, toIdx: Int): Array<StackTraceElement> {
|
||||
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<StackTraceElement>): Array<java.lang.StackTraceElement> {
|
||||
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
|
||||
|
12
djvm/src/main/kotlin/sandbox/java/lang/DJVMException.kt
Normal file
12
djvm/src/main/kotlin/sandbox/java/lang/DJVMException.kt
Normal file
@ -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
|
||||
}
|
@ -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()
|
||||
|
@ -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()
|
||||
class RuleViolationError(override val message: String?) : ThreadDeath()
|
24
djvm/src/test/java/net/corda/djvm/WithJava.java
Normal file
24
djvm/src/test/java/net/corda/djvm/WithJava.java
Normal file
@ -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 <T,R> ExecutionSummaryWithResult<R> run(
|
||||
SandboxExecutor<T, R> executor, Class<? extends Function<T,R>> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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<Integer, String[]> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
|
||||
ExecutionSummaryWithResult<String[]> 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<String, ExampleEnum> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
|
||||
ExecutionSummaryWithResult<ExampleEnum> 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<ExampleEnum, Boolean> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
|
||||
ExecutionSummaryWithResult<Boolean> 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<ExampleEnum, Integer> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
|
||||
ExecutionSummaryWithResult<Integer> 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<ExampleEnum, Boolean> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
|
||||
ExecutionSummaryWithResult<Boolean> output = WithJava.run(executor, UseEnumSet.class, ExampleEnum.ONE);
|
||||
assertThat(output.getResult()).isTrue();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static class AssertEnum implements Function<ExampleEnum, Boolean> {
|
||||
@Override
|
||||
public Boolean apply(ExampleEnum input) {
|
||||
return input.getClass().isEnum();
|
||||
}
|
||||
}
|
||||
|
||||
public static class TransformEnum implements Function<Integer, String[]> {
|
||||
@Override
|
||||
public String[] apply(Integer input) {
|
||||
return Stream.of(ExampleEnum.values()).map(ExampleEnum::name).toArray(String[]::new);
|
||||
}
|
||||
}
|
||||
|
||||
public static class FetchEnum implements Function<String, ExampleEnum> {
|
||||
public ExampleEnum apply(String input) {
|
||||
return ExampleEnum.valueOf(input);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UseEnumMap implements Function<ExampleEnum, Integer> {
|
||||
@Override
|
||||
public Integer apply(ExampleEnum input) {
|
||||
Map<ExampleEnum, String> map = new EnumMap<>(ExampleEnum.class);
|
||||
map.put(input, input.name());
|
||||
return map.size();
|
||||
}
|
||||
}
|
||||
|
||||
public static class UseEnumSet implements Function<ExampleEnum, Boolean> {
|
||||
@Override
|
||||
public Boolean apply(ExampleEnum input) {
|
||||
return EnumSet.allOf(ExampleEnum.class).contains(input);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<String, String[]> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
|
||||
ExecutionSummaryWithResult<String[]> 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<String, String> executor = new DeterministicSandboxExecutor<>(ctx.getConfiguration());
|
||||
|
||||
ExecutionSummaryWithResult<String> success = WithJava.run(executor, JavaWithCheckedExceptions.class, "http://localhost:8080/hello/world");
|
||||
assertThat(success.getResult()).isEqualTo("/hello/world");
|
||||
|
||||
ExecutionSummaryWithResult<String> 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<String, String[]> {
|
||||
@Override
|
||||
public String[] apply(String input) {
|
||||
List<String> 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<String, String> {
|
||||
@Override
|
||||
public String apply(String input) {
|
||||
try {
|
||||
return new URI(input).getPath();
|
||||
} catch (URISyntaxException e) {
|
||||
return "CATCH:" + e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
100
djvm/src/test/kotlin/net/corda/djvm/DJVMExceptionTest.kt
Normal file
100
djvm/src/test/kotlin/net/corda/djvm/DJVMExceptionTest.kt
Normal file
@ -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<Any?, sandbox.java.lang.Throwable> {
|
||||
override fun apply(input: Any?): sandbox.java.lang.Throwable? {
|
||||
return sandbox.java.lang.Throwable(input as? sandbox.java.lang.String)
|
||||
}
|
||||
}
|
||||
|
||||
class MultipleExceptionsTask : SandboxFunction<Any?, sandbox.java.lang.Throwable> {
|
||||
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<StackTraceElement>.toLineNumbers(): IntArray {
|
||||
return map(StackTraceElement::getLineNumber).toIntArray()
|
||||
}
|
@ -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)
|
||||
}
|
@ -37,22 +37,29 @@ abstract class TestBase {
|
||||
val ALL_EMITTERS = Discovery.find<Emitter>()
|
||||
|
||||
// We need at least these emitters to handle the Java API classes.
|
||||
@JvmField
|
||||
val BASIC_EMITTERS: List<Emitter> = listOf(
|
||||
ArgumentUnwrapper(),
|
||||
HandleExceptionUnwrapper(),
|
||||
ReturnTypeWrapper(),
|
||||
RewriteClassMethods(),
|
||||
StringConstantWrapper()
|
||||
StringConstantWrapper(),
|
||||
ThrowExceptionWrapper()
|
||||
)
|
||||
|
||||
val ALL_DEFINITION_PROVIDERS = Discovery.find<DefinitionProvider>()
|
||||
|
||||
// We need at least these providers to handle the Java API classes.
|
||||
@JvmField
|
||||
val BASIC_DEFINITION_PROVIDERS: List<DefinitionProvider> = listOf(StaticConstantRemover())
|
||||
|
||||
@JvmField
|
||||
val BLANK = emptySet<Any>()
|
||||
|
||||
@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)
|
||||
|
@ -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)
|
@ -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<Transaction, Unit> {
|
||||
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<Int, Int> {
|
||||
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
|
||||
}
|
||||
|
@ -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<String, Array<String>>(configuration)
|
||||
contractExecutor.run<ThrowAndCatchExample>("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<String, Array<String>>(configuration)
|
||||
contractExecutor.run<ThrowAndRethrowExample>("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<Int, String>(configuration)
|
||||
contractExecutor.run<TriggerJVMException>(-1).apply {
|
||||
assertThat(result)
|
||||
.isEqualTo("sandbox.java.lang.ArrayIndexOutOfBoundsException:-1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ThrowAndRethrowExample : Function<String, Array<String>> {
|
||||
override fun apply(input: String): Array<String> {
|
||||
val data = mutableListOf<String>()
|
||||
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<String, Array<String>> {
|
||||
override fun apply(input: String): Array<String> {
|
||||
val data = mutableListOf<String>()
|
||||
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<Int, String> {
|
||||
override fun apply(input: Int): String {
|
||||
return try {
|
||||
arrayOf(0, 1, 2)[input]
|
||||
"No Error"
|
||||
} catch (e: Exception) {
|
||||
e.javaClass.name + ':' + (e.message ?: "<MESSAGE MISSING>")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class MyBaseException(message: String) : Exception(message)
|
||||
class MyExampleException(message: String) : MyBaseException(message)
|
@ -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<T>` 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 <X>, 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<Issued<T>>, 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:
|
||||
|
@ -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
|
||||
-----------
|
||||
|
||||
|
@ -4,12 +4,15 @@
|
||||
<script type="text/javascript" src="_static/jquery.js"></script>
|
||||
<script type="text/javascript" src="_static/codesets.js"></script>
|
||||
|
||||
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
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
@ -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.
|
||||
|
||||
|
@ -12,7 +12,7 @@ public final class SummingOperation implements FlowAsyncOperation<Integer> {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public CordaFuture<Integer> execute() {
|
||||
public CordaFuture<Integer> execute(String deduplicationId) {
|
||||
return CordaFutureImplKt.doneFuture(this.a + this.b);
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ public final class SummingOperationThrowing implements FlowAsyncOperation<Intege
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public CordaFuture<Integer> execute() {
|
||||
public CordaFuture<Integer> execute(String deduplicationId) {
|
||||
throw new IllegalStateException("You shouldn't be calling me");
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ import net.corda.core.internal.executeAsync
|
||||
|
||||
// DOCSTART SummingOperation
|
||||
class SummingOperation(val a: Int, val b: Int) : FlowAsyncOperation<Int> {
|
||||
override fun execute(): CordaFuture<Int> {
|
||||
override fun execute(deduplicationId: String): CordaFuture<Int> {
|
||||
return doneFuture(a + b)
|
||||
}
|
||||
}
|
||||
@ -19,7 +19,7 @@ class SummingOperation(val a: Int, val b: Int) : FlowAsyncOperation<Int> {
|
||||
|
||||
// DOCSTART SummingOperationThrowing
|
||||
class SummingOperationThrowing(val a: Int, val b: Int) : FlowAsyncOperation<Int> {
|
||||
override fun execute(): CordaFuture<Int> {
|
||||
override fun execute(deduplicationId: String): CordaFuture<Int> {
|
||||
throw IllegalStateException("You shouldn't be calling me")
|
||||
}
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 171 KiB |
@ -43,6 +43,8 @@ Byzantine fault-tolerant (experimental)
|
||||
|
||||
A prototype BFT notary implementation based on `BFT-Smart <https://github.com/bft-smart/library>`_ is available. You can
|
||||
try it out on our `notary demo <https://github.com/corda/corda/tree/release-V3.1/samples/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.
|
||||
|
@ -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
|
||||
|
@ -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.")
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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<AsyncRetryFlow>()))
|
||||
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<RetryFlow>()))
|
||||
@ -229,6 +248,36 @@ class RetryFlow() : FlowLogic<String>(), IdempotentFlow {
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class AsyncRetryFlow() : FlowLogic<String>(), IdempotentFlow {
|
||||
companion object {
|
||||
object FIRST_STEP : ProgressTracker.Step("Step one")
|
||||
|
||||
fun tracker() = ProgressTracker(FIRST_STEP)
|
||||
|
||||
val deduplicationIds = mutableSetOf<String>()
|
||||
}
|
||||
|
||||
class RecordDeduplicationId: FlowAsyncOperation<String> {
|
||||
override fun execute(deduplicationId: String): CordaFuture<String> {
|
||||
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<String>(), IdempotentFlow {
|
||||
companion object {
|
||||
@ -248,4 +297,4 @@ class ThrowingFlow() : FlowLogic<String>(), IdempotentFlow {
|
||||
progressTracker.currentStep = FIRST_STEP
|
||||
return "Result"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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<S>(val configuration: NodeConfiguration,
|
||||
identityService::wellKnownPartyFromAnonymous,
|
||||
schemaService,
|
||||
cacheFactory)
|
||||
|
||||
init {
|
||||
// TODO Break cyclic dependency
|
||||
identityService.database = database
|
||||
@ -796,6 +799,10 @@ abstract class AbstractNode<S>(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<S>(val configuration: NodeConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
/** Installs a custom serialization filter defined by a notary service implementation. Only supported in dev mode. */
|
||||
private fun maybeInstallSerializationFilter(serviceClass: Class<out NotaryService>) {
|
||||
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<out NotaryService> {
|
||||
val loadedImplementations = cordappLoader.cordapps.mapNotNull { it.notaryService }
|
||||
log.debug("Notary service implementations found: ${loadedImplementations.joinToString(", ")}")
|
||||
|
@ -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<Config, Try<NodeConfiguration>> = 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 {
|
||||
|
@ -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" }
|
||||
}
|
||||
|
@ -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<MappedSchema> = 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<MappedSchema> = 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)
|
||||
}
|
||||
|
||||
|
@ -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).
|
||||
|
@ -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))
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user