Merge OS -> ENT

This commit is contained in:
szymonsztuka 2018-10-21 21:57:37 +01:00
commit e56d84fd5d
112 changed files with 2405 additions and 474 deletions

View File

@ -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
View File

@ -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>

View File

@ -9,6 +9,7 @@ buildCache {
enabled = !isCiServer
}
remote(HttpBuildCache) {
enabled = isCiServer
url = gradleBuildCacheURL
push = isCiServer
}

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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()
}
}

View File

@ -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

View File

@ -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()
}
}
}

View File

@ -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
/**

View File

@ -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]
*/

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -134,4 +134,3 @@ fun <V> Future<V>.getOrThrow(timeout: Duration? = null): V = try {
} catch (e: ExecutionException) {
throw e.cause!!
}

View File

@ -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 {

View File

@ -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/**'

View File

@ -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 {

View File

@ -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 {

View File

@ -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> {
}

View File

@ -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;
}
}

View File

@ -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");
}
}

View File

@ -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

View File

@ -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());
}

View 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 + ')' : "")
);
}
}

View File

@ -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();
}

View File

@ -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 {

View File

@ -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 {

View 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);
}
}

View File

@ -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 {

View File

@ -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> {

View File

@ -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();

View File

@ -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> {

View File

@ -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> {

View File

@ -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

View File

@ -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
)
}

View File

@ -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 }
}
}
}

View File

@ -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
}
}

View File

@ -35,4 +35,4 @@ class PrefixTree {
return false
}
}
}

View File

@ -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(),

View File

@ -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) {

View File

@ -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
}

View File

@ -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.
*/

View File

@ -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.

View File

@ -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()

View File

@ -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)

View File

@ -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, "")

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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;")
}
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
/**

View File

@ -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)
}

View File

@ -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?> {
/**

View File

@ -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

View 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
}

View File

@ -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()

View File

@ -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()

View 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);
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}
}

View 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()
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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)

View File

@ -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
}

View File

@ -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)

View File

@ -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:

View File

@ -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
-----------

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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);
}

View File

@ -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");
}

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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.")
}

View File

@ -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

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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)
}

View File

@ -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(

View File

@ -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
@ -154,7 +156,9 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
identityService::wellKnownPartyFromX500Name,
identityService::wellKnownPartyFromAnonymous,
schemaService,
configuration.dataSourceProperties,
cacheFactory)
init {
// TODO Break cyclic dependency
identityService.database = database
@ -796,6 +800,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 +817,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(", ")}")

View File

@ -155,7 +155,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 +388,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 +417,8 @@ 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) {
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) {
::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(::defaultSerialFilter)
}
protected open fun getVersionInfo(): VersionInfo {

View File

@ -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" }
}

View File

@ -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)
}

View File

@ -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).

View File

@ -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))

View File

@ -411,7 +411,10 @@ class StartedFlowTransition(
private fun executeAsyncOperation(flowIORequest: FlowIORequest.ExecuteAsyncOperation<*>): TransitionResult {
return builder {
actions.add(Action.ExecuteAsyncOperation(flowIORequest.operation))
// The `numberOfSuspends` is added to the deduplication ID in case an async
// operation is executed multiple times within the same flow.
val deduplicationId = context.id.toString() + ":" + currentState.checkpoint.numberOfSuspends.toString()
actions.add(Action.ExecuteAsyncOperation(deduplicationId, flowIORequest.operation))
FlowContinuation.ProcessEvents
}
}

Some files were not shown because too many files have changed in this diff Show More