Merge pull request #1184 from corda/mnesbit-generic-generatespend

Move generateSpend onto Cash contract. Add fully generic replacement.
This commit is contained in:
Matthew Nesbit
2017-08-10 15:25:40 +01:00
committed by GitHub
27 changed files with 577 additions and 464 deletions

View File

@ -8,13 +8,13 @@ import net.corda.core.node.services.Vault
import net.corda.core.node.services.VaultQueryException
import net.corda.core.node.services.vault.*
import net.corda.core.node.services.vault.QueryCriteria.CommonQueryCriteria
import net.corda.core.schemas.CommonSchemaV1
import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.PersistentStateRef
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.toHexString
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.toHexString
import net.corda.core.utilities.trace
import net.corda.core.schemas.CommonSchemaV1
import org.bouncycastle.asn1.x500.X500Name
import java.util.*
import javax.persistence.Tuple

View File

@ -8,28 +8,32 @@ import io.requery.kotlin.eq
import io.requery.kotlin.notNull
import io.requery.query.RowExpression
import net.corda.contracts.asset.Cash
import net.corda.contracts.asset.OnLedgerAsset
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.containsAny
import net.corda.core.crypto.toBase58String
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.internal.ThreadBox
import net.corda.core.internal.tee
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.StatesNotAvailableException
import net.corda.core.node.services.Vault
import net.corda.core.node.services.VaultService
import net.corda.core.node.services.vault.IQueryCriteriaParser
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.Sort
import net.corda.core.node.services.vault.SortAttribute
import net.corda.core.schemas.PersistentState
import net.corda.core.serialization.SerializationDefaults.STORAGE_CONTEXT
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.*
import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.toNonEmptySet
import net.corda.core.utilities.trace
import net.corda.node.services.database.RequeryConfiguration
import net.corda.node.services.database.parserTransactionIsolationLevel
import net.corda.node.services.statemachine.FlowStateMachineImpl
@ -42,10 +46,8 @@ import net.corda.node.utilities.wrapWithDatabaseTransaction
import rx.Observable
import rx.subjects.PublishSubject
import java.security.PublicKey
import java.sql.SQLException
import java.util.*
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import javax.persistence.criteria.Predicate
/**
* Currently, the node vault service is a very simple RDBMS backed implementation. It will change significantly when
@ -79,6 +81,7 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
// For use during publishing only.
val updatesPublisher: rx.Observer<Vault.Update<ContractState>> get() = _updatesPublisher.bufferUntilDatabaseCommit().tee(_rawUpdatesPublisher)
}
private val mutex = ThreadBox(InnerState())
private fun recordUpdate(update: Vault.Update<ContractState>): Vault.Update<ContractState> {
@ -334,123 +337,110 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
}
}
// coin selection retry loop counter, sleep (msecs) and lock for selecting states
val MAX_RETRIES = 5
val RETRY_SLEEP = 100
val spendLock: ReentrantLock = ReentrantLock()
// TODO We shouldn't need to rewrite the query if we could modify the defaults.
private class QueryEditor<out T : ContractState>(val services: ServiceHub,
val lockId: UUID,
val contractType: Class<out T>) : IQueryCriteriaParser {
var alreadyHasVaultQuery: Boolean = false
var modifiedCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(contractStateTypes = setOf(contractType),
softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)),
status = Vault.StateStatus.UNCONSUMED)
@Suspendable
override fun <T : ContractState> unconsumedStatesForSpending(amount: Amount<Currency>, onlyFromIssuerParties: Set<AbstractParty>?, notary: Party?, lockId: UUID, withIssuerRefs: Set<OpaqueBytes>?): List<StateAndRef<T>> {
val issuerKeysStr = onlyFromIssuerParties?.fold("") { left, right -> left + "('${right.owningKey.toBase58String()}')," }?.dropLast(1)
val issuerRefsStr = withIssuerRefs?.fold("") { left, right -> left + "('${right.bytes.toHexString()}')," }?.dropLast(1)
val stateAndRefs = mutableListOf<StateAndRef<T>>()
// TODO: Need to provide a database provider independent means of performing this function.
// We are using an H2 specific means of selecting a minimum set of rows that match a request amount of coins:
// 1) There is no standard SQL mechanism of calculating a cumulative total on a field and restricting row selection on the
// running total of such an accumulator
// 2) H2 uses session variables to perform this accumulator function:
// http://www.h2database.com/html/functions.html#set
// 3) H2 does not support JOIN's in FOR UPDATE (hence we are forced to execute 2 queries)
for (retryCount in 1..MAX_RETRIES) {
spendLock.withLock {
val statement = configuration.jdbcSession().createStatement()
try {
statement.execute("CALL SET(@t, 0);")
// we select spendable states irrespective of lock but prioritised by unlocked ones (Eg. null)
// the softLockReserve update will detect whether we try to lock states locked by others
val selectJoin = """
SELECT vs.transaction_id, vs.output_index, vs.contract_state, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id
FROM vault_states AS vs, contract_cash_states AS ccs
WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index
AND vs.state_status = 0
AND ccs.ccy_code = '${amount.token}' and @t < ${amount.quantity}
AND (vs.lock_id = '$lockId' OR vs.lock_id is null)
""" +
(if (notary != null)
" AND vs.notary_key = '${notary.owningKey.toBase58String()}'" else "") +
(if (issuerKeysStr != null)
" AND ccs.issuer_key IN ($issuerKeysStr)" else "") +
(if (issuerRefsStr != null)
" AND ccs.issuer_ref IN ($issuerRefsStr)" else "")
// Retrieve spendable state refs
val rs = statement.executeQuery(selectJoin)
stateAndRefs.clear()
log.debug(selectJoin)
var totalPennies = 0L
while (rs.next()) {
val txHash = SecureHash.parse(rs.getString(1))
val index = rs.getInt(2)
val stateRef = StateRef(txHash, index)
val state = rs.getBytes(3).deserialize<TransactionState<T>>(context = STORAGE_CONTEXT)
val pennies = rs.getLong(4)
totalPennies = rs.getLong(5)
val rowLockId = rs.getString(6)
stateAndRefs.add(StateAndRef(state, stateRef))
log.trace { "ROW: $rowLockId ($lockId): $stateRef : $pennies ($totalPennies)" }
}
if (stateAndRefs.isNotEmpty() && totalPennies >= amount.quantity) {
// we should have a minimum number of states to satisfy our selection `amount` criteria
log.trace("Coin selection for $amount retrieved ${stateAndRefs.count()} states totalling $totalPennies pennies: $stateAndRefs")
// update database
softLockReserve(lockId, (stateAndRefs.map { it.ref }).toNonEmptySet())
return stateAndRefs
}
log.trace("Coin selection requested $amount but retrieved $totalPennies pennies with state refs: ${stateAndRefs.map { it.ref }}")
// retry as more states may become available
} catch (e: SQLException) {
log.error("""Failed retrieving unconsumed states for: amount [$amount], onlyFromIssuerParties [$onlyFromIssuerParties], notary [$notary], lockId [$lockId]
$e.
""")
} catch (e: StatesNotAvailableException) {
stateAndRefs.clear()
log.warn(e.message)
// retry only if there are locked states that may become available again (or consumed with change)
} finally {
statement.close()
}
}
log.warn("Coin selection failed on attempt $retryCount")
// TODO: revisit the back off strategy for contended spending.
if (retryCount != MAX_RETRIES) {
FlowStateMachineImpl.sleep(RETRY_SLEEP * retryCount.toLong())
}
override fun parseCriteria(criteria: QueryCriteria.CommonQueryCriteria): Collection<Predicate> {
modifiedCriteria = criteria
return emptyList()
}
log.warn("Insufficient spendable states identified for $amount")
return stateAndRefs
override fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection<Predicate> {
modifiedCriteria = criteria
return emptyList()
}
override fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection<Predicate> {
modifiedCriteria = criteria
return emptyList()
}
override fun <L : PersistentState> parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria<L>): Collection<Predicate> {
modifiedCriteria = criteria
return emptyList()
}
override fun parseCriteria(criteria: QueryCriteria.VaultQueryCriteria): Collection<Predicate> {
modifiedCriteria = criteria.copy(contractStateTypes = setOf(contractType),
softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)),
status = Vault.StateStatus.UNCONSUMED)
alreadyHasVaultQuery = true
return emptyList()
}
override fun parseOr(left: QueryCriteria, right: QueryCriteria): Collection<Predicate> {
parse(left)
val modifiedLeft = modifiedCriteria
parse(right)
val modifiedRight = modifiedCriteria
modifiedCriteria = modifiedLeft.or(modifiedRight)
return emptyList()
}
override fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection<Predicate> {
parse(left)
val modifiedLeft = modifiedCriteria
parse(right)
val modifiedRight = modifiedCriteria
modifiedCriteria = modifiedLeft.and(modifiedRight)
return emptyList()
}
override fun parse(criteria: QueryCriteria, sorting: Sort?): Collection<Predicate> {
val basicQuery = modifiedCriteria
criteria.visit(this)
modifiedCriteria = if (alreadyHasVaultQuery) modifiedCriteria else criteria.and(basicQuery)
return emptyList()
}
fun queryForEligibleStates(criteria: QueryCriteria): Vault.Page<T> {
val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF)
val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC)))
parse(criteria, sorter)
return services.vaultQueryService.queryBy(contractType, modifiedCriteria, sorter)
}
}
/**
* Generate a transaction that moves an amount of currency to the given pubkey.
*
* @param onlyFromParties if non-null, the asset states will be filtered to only include those issued by the set
* of given parties. This can be useful if the party you're trying to pay has expectations
* about which type of asset claims they are willing to accept.
*/
@Suspendable
override fun generateSpend(tx: TransactionBuilder,
amount: Amount<Currency>,
to: AbstractParty,
onlyFromParties: Set<AbstractParty>?): Pair<TransactionBuilder, List<PublicKey>> {
// Retrieve unspent and unlocked cash states that meet our spending criteria.
val acceptableCoins = unconsumedStatesForSpending<Cash.State>(amount, onlyFromParties, tx.notary, tx.lockId)
return OnLedgerAsset.generateSpend(tx, amount, to, acceptableCoins,
{ state, quantity, owner -> deriveState(state, quantity, owner) },
{ Cash().generateMoveCommand() })
}
@Throws(StatesNotAvailableException::class)
override fun <T : FungibleAsset<U>, U : Any> tryLockFungibleStatesForSpending(lockId: UUID,
eligibleStatesQuery: QueryCriteria,
amount: Amount<U>,
contractType: Class<out T>): List<StateAndRef<T>> {
if (amount.quantity == 0L) {
return emptyList()
}
private fun deriveState(txState: TransactionState<Cash.State>, amount: Amount<Issued<Currency>>, owner: AbstractParty)
= txState.copy(data = txState.data.copy(amount = amount, owner = owner))
// TODO This helper code re-writes the query to alter the defaults on things such as soft locks
// and then runs the query. Ideally we would not need to do this.
val results = QueryEditor(services, lockId, contractType).queryForEligibleStates(eligibleStatesQuery)
var claimedAmount = 0L
val claimedStates = mutableListOf<StateAndRef<T>>()
for (state in results.states) {
val issuedAssetToken = state.state.data.amount.token
if (issuedAssetToken.product == amount.token) {
claimedStates += state
claimedAmount += state.state.data.amount.quantity
if (claimedAmount > amount.quantity) {
break
}
}
}
if (claimedStates.isEmpty() || claimedAmount < amount.quantity) {
return emptyList()
}
softLockReserve(lockId, claimedStates.map { it.ref }.toNonEmptySet())
return claimedStates
}
// TODO : Persists this in DB.
private val authorisedUpgrade = mutableMapOf<StateRef, Class<out UpgradedContract<*, *>>>()

View File

@ -1,42 +1,50 @@
package net.corda.node.services.vault;
import com.google.common.collect.*;
import net.corda.contracts.*;
import net.corda.contracts.asset.*;
import com.google.common.collect.ImmutableSet;
import kotlin.Pair;
import net.corda.contracts.DealState;
import net.corda.contracts.asset.Cash;
import net.corda.core.contracts.*;
import net.corda.core.crypto.*;
import net.corda.core.identity.*;
import net.corda.core.messaging.*;
import net.corda.core.node.services.*;
import net.corda.core.crypto.EncodingUtils;
import net.corda.core.identity.AbstractParty;
import net.corda.core.messaging.DataFeed;
import net.corda.core.node.services.Vault;
import net.corda.core.node.services.VaultQueryException;
import net.corda.core.node.services.VaultQueryService;
import net.corda.core.node.services.VaultService;
import net.corda.core.node.services.vault.*;
import net.corda.core.node.services.vault.QueryCriteria.*;
import net.corda.core.schemas.*;
import net.corda.core.transactions.*;
import net.corda.core.utilities.*;
import net.corda.node.services.database.*;
import net.corda.node.services.schema.*;
import net.corda.node.utilities.*;
import net.corda.schemas.*;
import net.corda.testing.*;
import net.corda.testing.contracts.*;
import net.corda.testing.node.*;
import net.corda.testing.schemas.*;
import org.jetbrains.annotations.*;
import org.junit.*;
import net.corda.core.node.services.vault.QueryCriteria.LinearStateQueryCriteria;
import net.corda.core.node.services.vault.QueryCriteria.VaultCustomQueryCriteria;
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria;
import net.corda.core.utilities.OpaqueBytes;
import net.corda.node.utilities.CordaPersistence;
import net.corda.schemas.CashSchemaV1;
import net.corda.testing.TestConstants;
import net.corda.testing.TestDependencyInjectionBase;
import net.corda.testing.contracts.DummyLinearContract;
import net.corda.testing.contracts.VaultFiller;
import net.corda.testing.node.MockServices;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import rx.Observable;
import java.io.*;
import java.lang.reflect.*;
import java.io.IOException;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.util.*;
import java.util.stream.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static net.corda.contracts.asset.CashKt.*;
import static net.corda.core.node.services.vault.QueryCriteriaUtils.*;
import static net.corda.core.utilities.ByteArrays.*;
import static net.corda.node.utilities.CordaPersistenceKt.*;
import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER;
import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER_KEY;
import static net.corda.core.node.services.vault.QueryCriteriaUtils.DEFAULT_PAGE_NUM;
import static net.corda.core.node.services.vault.QueryCriteriaUtils.MAX_PAGE_SIZE;
import static net.corda.core.utilities.ByteArrays.toHexString;
import static net.corda.testing.CoreTestUtils.*;
import static net.corda.testing.node.MockServicesKt.*;
import static org.assertj.core.api.Assertions.*;
import static net.corda.testing.node.MockServicesKt.makeTestDatabaseAndMockServices;
import static org.assertj.core.api.Assertions.assertThat;
public class VaultQueryJavaTests extends TestDependencyInjectionBase {
@ -47,38 +55,13 @@ public class VaultQueryJavaTests extends TestDependencyInjectionBase {
@Before
public void setUp() {
Properties dataSourceProps = makeTestDataSourceProperties(SecureHash.randomSHA256().toString());
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties());
database.transaction(statement -> {
Set<MappedSchema> customSchemas = new HashSet<>(Collections.singletonList(DummyLinearStateSchemaV1.INSTANCE));
HibernateConfiguration hibernateConfig = new HibernateConfiguration(new NodeSchemaService(customSchemas), makeTestDatabaseProperties());
services = new MockServices(getMEGA_CORP_KEY()) {
@NotNull
@Override
public VaultService getVaultService() {
if (vaultSvc != null) return vaultSvc;
return makeVaultService(dataSourceProps, hibernateConfig);
}
@NotNull
@Override
public VaultQueryService getVaultQueryService() {
return new HibernateVaultQueryImpl(hibernateConfig, vaultSvc.getUpdatesPublisher());
}
@Override
public void recordTransactions(@NotNull Iterable<SignedTransaction> txs) {
for (SignedTransaction stx : txs) {
getValidatedTransactions().addTransaction(stx);
}
Stream<WireTransaction> wtxn = StreamSupport.stream(txs.spliterator(), false).map(SignedTransaction::getTx);
vaultSvc.notifyAll(wtxn.collect(Collectors.toList()));
}
};
vaultSvc = services.getVaultService();
vaultQuerySvc = services.getVaultQueryService();
return services;
});
ArrayList<KeyPair> keys = new ArrayList<>();
keys.add(getMEGA_CORP_KEY());
Pair<CordaPersistence, MockServices> databaseAndServices = makeTestDatabaseAndMockServices(Collections.EMPTY_SET, keys);
database = databaseAndServices.getFirst();
services = databaseAndServices.getSecond();
vaultSvc = services.getVaultService();
vaultQuerySvc = services.getVaultQueryService();
}
@After

View File

@ -148,8 +148,8 @@ class TwoPartyTradeFlowTests {
bobNode.disableDBCloseOnStop()
val cashStates = bobNode.database.transaction {
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, notaryNode.info.notaryIdentity, 3, 3)
}
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, notaryNode.info.notaryIdentity, 3, 3)
}
val alicesFakePaper = aliceNode.database.transaction {
fillUpForSeller(false, cpIssuer, aliceNode.info.legalIdentity,
@ -168,7 +168,7 @@ class TwoPartyTradeFlowTests {
}
val (bobStateMachine, aliceResult) = runBuyerAndSeller(notaryNode, aliceNode, bobNode,
"alice's paper".outputStateAndRef())
"alice's paper".outputStateAndRef())
assertEquals(aliceResult.getOrThrow(), bobStateMachine.getOrThrow().resultFuture.getOrThrow())
@ -533,11 +533,11 @@ class TwoPartyTradeFlowTests {
override fun call(): SignedTransaction {
send(buyer, Pair(notary.notaryIdentity, price))
return subFlow(Seller(
buyer,
notary,
assetToSell,
price,
me.party))
buyer,
notary,
assetToSell,
price,
me.party))
}
}

View File

@ -3,10 +3,12 @@ package net.corda.node.services.database
import net.corda.contracts.asset.Cash
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
import net.corda.contracts.asset.DummyFungibleContract
import net.corda.contracts.asset.sumCash
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.toBase58String
import net.corda.core.node.services.Vault
import net.corda.core.node.services.VaultQueryService
import net.corda.core.node.services.VaultService
import net.corda.core.schemas.CommonSchemaV1
import net.corda.core.schemas.PersistentStateRef
@ -14,6 +16,7 @@ import net.corda.core.serialization.deserialize
import net.corda.core.transactions.SignedTransaction
import net.corda.node.services.schema.HibernateObserver
import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.services.vault.HibernateVaultQueryImpl
import net.corda.node.services.vault.NodeVaultService
import net.corda.node.services.vault.VaultSchemaV1
import net.corda.node.utilities.CordaPersistence
@ -38,6 +41,7 @@ import org.junit.After
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import java.math.BigDecimal
import java.time.Instant
import java.util.*
import javax.persistence.EntityManager
@ -126,7 +130,8 @@ class HibernateConfigurationTest : TestDependencyInjectionBase() {
// execute query
val queryResults = entityManager.createQuery(criteriaQuery).resultList
assertThat(queryResults.size).isEqualTo(6)
val coins = queryResults.map { it.contractState.deserialize<TransactionState<Cash.State>>().data }.sumCash()
assertThat(coins.toDecimal() >= BigDecimal("50.00"))
}
@Test

View File

@ -1,39 +1,40 @@
package net.corda.node.services.vault
import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.asset.Cash
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
import net.corda.contracts.asset.sumCash
import net.corda.contracts.getCashBalance
import net.corda.core.contracts.*
import net.corda.core.crypto.generateKeyPair
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.node.services.StatesNotAvailableException
import net.corda.core.node.services.Vault
import net.corda.core.node.services.VaultQueryService
import net.corda.core.node.services.VaultService
import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.QueryCriteria.*
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.toNonEmptySet
import net.corda.node.services.database.HibernateConfiguration
import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.utilities.CordaPersistence
import net.corda.node.utilities.configureDatabase
import net.corda.testing.*
import net.corda.testing.contracts.fillWithSomeTestCash
import net.corda.testing.node.MockServices
import net.corda.testing.node.makeTestDataSourceProperties
import net.corda.testing.node.makeTestDatabaseProperties
import net.corda.testing.node.makeTestDatabaseAndMockServices
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.After
import org.junit.Before
import org.junit.Test
import rx.observers.TestSubscriber
import java.math.BigDecimal
import java.util.*
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
@ -50,23 +51,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
@Before
fun setUp() {
LogHelper.setLevel(NodeVaultService::class)
val dataSourceProps = makeTestDataSourceProperties()
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties())
database.transaction {
val hibernateConfig = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties())
services = object : MockServices() {
override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
for (stx in txs) {
validatedTransactions.addTransaction(stx)
}
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
vaultService.notifyAll(txs.map { it.tx })
}
override val vaultQueryService : VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
}
}
val databaseAndServices = makeTestDatabaseAndMockServices()
database = databaseAndServices.first
services = databaseAndServices.second
}
@After
@ -75,6 +62,25 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
LogHelper.reset(NodeVaultService::class)
}
@Suspendable
private fun VaultService.unconsumedCashStatesForSpending(amount: Amount<Currency>,
onlyFromIssuerParties: Set<AbstractParty>? = null,
notary: Party? = null,
lockId: UUID = UUID.randomUUID(),
withIssuerRefs: Set<OpaqueBytes>? = null): List<StateAndRef<Cash.State>> {
val notaryName = if (notary != null) listOf(notary.name) else null
var baseCriteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(notaryName = notaryName)
if (onlyFromIssuerParties != null || withIssuerRefs != null) {
baseCriteria = baseCriteria.and(QueryCriteria.FungibleAssetQueryCriteria(
issuerPartyName = onlyFromIssuerParties?.toList(),
issuerRef = withIssuerRefs?.toList()))
}
return tryLockFungibleStatesForSpending(lockId, baseCriteria, amount, Cash.State::class.java)
}
@Test
fun `states not local to instance`() {
database.transaction {
@ -308,7 +314,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
assertThat(unconsumedStates).hasSize(1)
val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending<Cash.State>(100.DOLLARS, lockId = UUID.randomUUID())
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(100.DOLLARS)
spendableStatesUSD.forEach(::println)
assertThat(spendableStatesUSD).hasSize(1)
assertThat(spendableStatesUSD[0].state.data.amount.quantity).isEqualTo(100L * 100)
@ -324,12 +330,13 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (DUMMY_CASH_ISSUER))
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 1, 1, Random(0L), issuedBy = (BOC.ref(1)), issuerKey = BOC_KEY)
val spendableStatesUSD = vaultSvc.unconsumedStatesForSpending<Cash.State>(200.DOLLARS, lockId = UUID.randomUUID(),
onlyFromIssuerParties = setOf(DUMMY_CASH_ISSUER.party, BOC)).toList()
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(200.DOLLARS,
onlyFromIssuerParties = setOf(DUMMY_CASH_ISSUER.party, BOC))
spendableStatesUSD.forEach(::println)
assertThat(spendableStatesUSD).hasSize(2)
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer).isEqualTo(DUMMY_CASH_ISSUER)
assertThat(spendableStatesUSD[1].state.data.amount.token.issuer).isEqualTo(BOC.ref(1))
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer).isIn(DUMMY_CASH_ISSUER, BOC.ref(1))
assertThat(spendableStatesUSD[1].state.data.amount.token.issuer).isIn(DUMMY_CASH_ISSUER, BOC.ref(1))
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer).isNotEqualTo(spendableStatesUSD[1].state.data.amount.token.issuer)
}
}
@ -345,12 +352,13 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
assertThat(unconsumedStates).hasSize(4)
val spendableStatesUSD = vaultSvc.unconsumedStatesForSpending<Cash.State>(200.DOLLARS, lockId = UUID.randomUUID(),
onlyFromIssuerParties = setOf(BOC), withIssuerRefs = setOf(OpaqueBytes.of(1), OpaqueBytes.of(2))).toList()
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(200.DOLLARS,
onlyFromIssuerParties = setOf(BOC), withIssuerRefs = setOf(OpaqueBytes.of(1), OpaqueBytes.of(2)))
assertThat(spendableStatesUSD).hasSize(2)
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.party).isEqualTo(BOC)
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.reference).isEqualTo(BOC.ref(1).reference)
assertThat(spendableStatesUSD[1].state.data.amount.token.issuer.reference).isEqualTo(BOC.ref(2).reference)
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.reference).isIn(BOC.ref(1).reference, BOC.ref(2).reference)
assertThat(spendableStatesUSD[1].state.data.amount.token.issuer.reference).isIn(BOC.ref(1).reference, BOC.ref(2).reference)
assertThat(spendableStatesUSD[0].state.data.amount.token.issuer.reference).isNotEqualTo(spendableStatesUSD[1].state.data.amount.token.issuer.reference)
}
}
@ -363,9 +371,9 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
assertThat(unconsumedStates).hasSize(1)
val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending<Cash.State>(110.DOLLARS, lockId = UUID.randomUUID())
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(110.DOLLARS)
spendableStatesUSD.forEach(::println)
assertThat(spendableStatesUSD).hasSize(1)
assertThat(spendableStatesUSD).hasSize(0)
val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY))
assertThat(vaultQuery.queryBy<Cash.State>(criteriaLocked).states).hasSize(0)
}
@ -380,7 +388,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
val unconsumedStates = vaultQuery.queryBy<Cash.State>().states
assertThat(unconsumedStates).hasSize(2)
val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending<Cash.State>(1.DOLLARS, lockId = UUID.randomUUID())
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(1.DOLLARS)
spendableStatesUSD.forEach(::println)
assertThat(spendableStatesUSD).hasSize(1)
assertThat(spendableStatesUSD[0].state.data.amount.quantity).isGreaterThanOrEqualTo(100L)
@ -397,16 +405,30 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 10, 10, Random(0L))
services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 10, 10, Random(0L))
var unlockedStates = 30
val allStates = vaultQuery.queryBy<Cash.State>().states
assertThat(allStates).hasSize(30)
assertThat(allStates).hasSize(unlockedStates)
var lockedCount = 0
for (i in 1..5) {
val spendableStatesUSD = (vaultSvc as NodeVaultService).unconsumedStatesForSpending<Cash.State>(20.DOLLARS, lockId = UUID.randomUUID())
val lockId = UUID.randomUUID()
val spendableStatesUSD = vaultSvc.unconsumedCashStatesForSpending(20.DOLLARS, lockId = lockId)
spendableStatesUSD.forEach(::println)
assertThat(spendableStatesUSD.size <= unlockedStates)
unlockedStates -= spendableStatesUSD.size
val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.SPECIFIED, listOf(lockId)))
val lockedStates = vaultQuery.queryBy<Cash.State>(criteriaLocked).states
if (spendableStatesUSD.isNotEmpty()) {
assertEquals(spendableStatesUSD.size, lockedStates.size)
val lockedTotal = lockedStates.map { it.state.data }.sumCash()
val foundAmount = spendableStatesUSD.map { it.state.data }.sumCash()
assertThat(foundAmount.toDecimal() >= BigDecimal("20.00"))
assertThat(lockedTotal == foundAmount)
lockedCount += lockedStates.size
}
}
// note only 3 spend attempts succeed with a total of 8 states
val criteriaLocked = VaultQueryCriteria(softLockingCondition = SoftLockingCondition(SoftLockingType.LOCKED_ONLY))
assertThat(vaultQuery.queryBy<Cash.State>(criteriaLocked).states).hasSize(8)
assertThat(vaultQuery.queryBy<Cash.State>(criteriaLocked).states).hasSize(lockedCount)
}
}
@ -484,7 +506,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
database.transaction {
val moveTx = TransactionBuilder(services.myInfo.legalIdentity).apply {
service.generateSpend(this, Amount(1000, GBP), thirdPartyIdentity)
Cash.generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity)
}.toWireTransaction()
service.notify(moveTx)
}
@ -530,7 +552,7 @@ class NodeVaultServiceTest : TestDependencyInjectionBase() {
// Move cash
val moveTx = database.transaction {
TransactionBuilder(newNotary).apply {
service.generateSpend(this, Amount(1000, GBP), thirdPartyIdentity)
Cash.generateSpend(services, this, Amount(1000, GBP), thirdPartyIdentity)
}.toWireTransaction()
}

View File

@ -1,23 +1,19 @@
package net.corda.node.services.vault
import net.corda.contracts.*
import net.corda.contracts.CommercialPaper
import net.corda.contracts.Commodity
import net.corda.contracts.DealState
import net.corda.contracts.asset.Cash
import net.corda.contracts.asset.DUMMY_CASH_ISSUER
import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.entropyToKeyPair
import net.corda.core.crypto.toBase58String
import net.corda.core.utilities.days
import net.corda.core.identity.Party
import net.corda.core.node.services.*
import net.corda.core.node.services.vault.*
import net.corda.core.node.services.vault.QueryCriteria.*
import net.corda.core.utilities.seconds
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.toHexString
import net.corda.node.services.database.HibernateConfiguration
import net.corda.node.services.schema.NodeSchemaService
import net.corda.core.utilities.*
import net.corda.node.utilities.CordaPersistence
import net.corda.node.utilities.configureDatabase
import net.corda.schemas.CashSchemaV1
@ -27,7 +23,7 @@ import net.corda.schemas.SampleCashSchemaV3
import net.corda.testing.*
import net.corda.testing.contracts.*
import net.corda.testing.node.MockServices
import net.corda.testing.node.makeTestDataSourceProperties
import net.corda.testing.node.makeTestDatabaseAndMockServices
import net.corda.testing.node.makeTestDatabaseProperties
import net.corda.testing.schemas.DummyLinearStateSchemaV1
import org.assertj.core.api.Assertions
@ -54,24 +50,9 @@ class VaultQueryTests : TestDependencyInjectionBase() {
@Before
fun setUp() {
val dataSourceProps = makeTestDataSourceProperties()
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties())
database.transaction {
val customSchemas = setOf(CommercialPaperSchemaV1, DummyLinearStateSchemaV1)
val hibernateConfig = HibernateConfiguration(NodeSchemaService(customSchemas), makeTestDatabaseProperties())
services = object : MockServices(MEGA_CORP_KEY) {
override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
for (stx in txs) {
validatedTransactions.addTransaction(stx)
}
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
vaultService.notifyAll(txs.map { it.tx })
}
override val vaultQueryService : VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
}
}
val databaseAndServices = makeTestDatabaseAndMockServices(keys = listOf(MEGA_CORP_KEY))
database = databaseAndServices.first
services = databaseAndServices.second
}
@After
@ -97,7 +78,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
_database.transaction {
// create new states
services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 10, 10, Random(0L))
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
val linearStatesXYZ = services.fillWithSomeTestLinearStates(1, "XYZ")
val linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL")
services.fillWithSomeTestLinearStates(3, "ABC")
@ -239,15 +220,15 @@ class VaultQueryTests : TestDependencyInjectionBase() {
fun `unconsumed cash states sorted by state ref`() {
database.transaction {
var stateRefs : MutableList<StateRef> = mutableListOf()
val stateRefs: MutableList<StateRef> = mutableListOf()
val issuedStates = services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
val issuedStateRefs = issuedStates.states.map { it.ref }.toList()
stateRefs.addAll(issuedStateRefs)
val spentStates = services.consumeCash(25.DOLLARS)
var consumedStateRefs = spentStates.consumed.map { it.ref }.toList()
var producedStateRefs = spentStates.produced.map { it.ref }.toList()
val consumedStateRefs = spentStates.consumed.map { it.ref }.toList()
val producedStateRefs = spentStates.produced.map { it.ref }.toList()
stateRefs.addAll(consumedStateRefs.plus(producedStateRefs))
val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF)
@ -271,8 +252,9 @@ class VaultQueryTests : TestDependencyInjectionBase() {
fun `unconsumed cash states sorted by state ref txnId and index`() {
database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
services.consumeCash(10.DOLLARS)
services.consumeCash(10.DOLLARS)
val consumed = mutableSetOf<SecureHash>()
services.consumeCash(10.DOLLARS).consumed.forEach { consumed += it.ref.txhash }
services.consumeCash(10.DOLLARS).consumed.forEach { consumed += it.ref.txhash }
val sortAttributeTxnId = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_TXN_ID)
val sortAttributeIndex = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF_INDEX)
@ -283,13 +265,11 @@ class VaultQueryTests : TestDependencyInjectionBase() {
results.statesMetadata.forEach {
println(" ${it.ref}")
assertThat(it.status).isEqualTo(Vault.StateStatus.UNCONSUMED)
}
// explicit sort order asc by txnId and then index:
// order by
// vaultschem1_.transaction_id asc,
// vaultschem1_.output_index asc
assertThat(results.states).hasSize(9) // -2 CONSUMED + 1 NEW UNCONSUMED (change)
val sorted = results.states.sortedBy { it.ref.toString() }
assertThat(results.states).isEqualTo(sorted)
assertThat(results.states).allSatisfy { !consumed.contains(it.ref.txhash) }
}
}
@ -411,7 +391,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
}
}
val CASH_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) }
val CASH_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(21)) }
val CASH_NOTARY: Party get() = Party(X500Name("CN=Cash Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), CASH_NOTARY_KEY.public)
@Test
@ -870,7 +850,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
fun `aggregate functions count by contract type and state status`() {
database.transaction {
// create new states
services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 10, 10, Random(0L))
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L))
val linearStatesXYZ = services.fillWithSomeTestLinearStates(1, "XYZ")
val linearStatesJKL = services.fillWithSomeTestLinearStates(2, "JKL")
services.fillWithSomeTestLinearStates(3, "ABC")
@ -896,14 +876,14 @@ class VaultQueryTests : TestDependencyInjectionBase() {
services.consumeLinearStates(linearStatesXYZ.states.toList())
services.consumeLinearStates(linearStatesJKL.states.toList())
services.consumeDeals(dealStates.states.filter { it.state.data.ref == "456" })
services.consumeCash(50.DOLLARS)
val cashUpdates = services.consumeCash(50.DOLLARS)
// UNCONSUMED states (default)
// count fungible assets
val countCriteriaUnconsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.UNCONSUMED)
val fungibleStateCountUnconsumed = vaultQuerySvc.queryBy<FungibleAsset<*>>(countCriteriaUnconsumed).otherResults.single() as Long
assertThat(fungibleStateCountUnconsumed).isEqualTo(5L)
assertThat(fungibleStateCountUnconsumed.toInt()).isEqualTo(10 - cashUpdates.consumed.size + cashUpdates.produced.size)
// count linear states
val linearStateCountUnconsumed = vaultQuerySvc.queryBy<LinearState>(countCriteriaUnconsumed).otherResults.single() as Long
@ -918,7 +898,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
// count fungible assets
val countCriteriaConsumed = QueryCriteria.VaultCustomQueryCriteria(count, Vault.StateStatus.CONSUMED)
val fungibleStateCountConsumed = vaultQuerySvc.queryBy<FungibleAsset<*>>(countCriteriaConsumed).otherResults.single() as Long
assertThat(fungibleStateCountConsumed).isEqualTo(6L)
assertThat(fungibleStateCountConsumed.toInt()).isEqualTo(cashUpdates.consumed.size)
// count linear states
val linearStateCountConsumed = vaultQuerySvc.queryBy<LinearState>(countCriteriaConsumed).otherResults.single() as Long
@ -962,7 +942,7 @@ class VaultQueryTests : TestDependencyInjectionBase() {
fun `states consumed after time`() {
database.transaction {
services.fillWithSomeTestCash(100.DOLLARS, CASH_NOTARY, 3, 3, Random(0L))
services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L))
services.fillWithSomeTestLinearStates(10)
services.fillWithSomeTestDeals(listOf("123", "456", "789"))

View File

@ -11,17 +11,12 @@ import net.corda.core.node.services.VaultService
import net.corda.core.node.services.queryBy
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.node.services.database.HibernateConfiguration
import net.corda.node.services.schema.NodeSchemaService
import net.corda.node.utilities.CordaPersistence
import net.corda.node.utilities.configureDatabase
import net.corda.testing.*
import net.corda.testing.contracts.*
import net.corda.testing.node.MockServices
import net.corda.testing.node.makeTestDataSourceProperties
import net.corda.testing.node.makeTestDatabaseProperties
import net.corda.testing.node.makeTestDatabaseAndMockServices
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.After
@ -44,23 +39,9 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
@Before
fun setUp() {
LogHelper.setLevel(VaultWithCashTest::class)
val dataSourceProps = makeTestDataSourceProperties()
database = configureDatabase(dataSourceProps, makeTestDatabaseProperties())
database.transaction {
val hibernateConfig = HibernateConfiguration(NodeSchemaService(), makeTestDatabaseProperties())
services = object : MockServices() {
override val vaultService: VaultService = makeVaultService(dataSourceProps, hibernateConfig)
override fun recordTransactions(txs: Iterable<SignedTransaction>) {
for (stx in txs) {
validatedTransactions.addTransaction(stx)
}
// Refactored to use notifyAll() as we have no other unit test for that method with multiple transactions.
vaultService.notifyAll(txs.map { it.tx })
}
override val vaultQueryService : VaultQueryService = HibernateVaultQueryImpl(hibernateConfig, vaultService.updatesPublisher)
}
}
val databaseAndServices = makeTestDatabaseAndMockServices()
database = databaseAndServices.first
services = databaseAndServices.second
}
@After
@ -103,7 +84,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
// A tx that spends our money.
val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY)
vault.generateSpend(spendTXBuilder, 80.DOLLARS, BOB)
Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB)
val spendPTX = services.signInitialTransaction(spendTXBuilder, freshKey)
val spendTX = notaryServices.addSignature(spendPTX)
@ -151,7 +132,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
database.transaction {
try {
val txn1Builder = TransactionBuilder(DUMMY_NOTARY)
vault.generateSpend(txn1Builder, 60.DOLLARS, BOB)
Cash.generateSpend(services, txn1Builder, 60.DOLLARS, BOB)
val ptxn1 = notaryServices.signInitialTransaction(txn1Builder)
val txn1 = services.addSignature(ptxn1, freshKey)
println("txn1: ${txn1.id} spent ${((txn1.tx.outputs[0].data) as Cash.State).amount}")
@ -187,7 +168,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
database.transaction {
try {
val txn2Builder = TransactionBuilder(DUMMY_NOTARY)
vault.generateSpend(txn2Builder, 80.DOLLARS, BOB)
Cash.generateSpend(services, txn2Builder, 80.DOLLARS, BOB)
val ptxn2 = notaryServices.signInitialTransaction(txn2Builder)
val txn2 = services.addSignature(ptxn2, freshKey)
println("txn2: ${txn2.id} spent ${((txn2.tx.outputs[0].data) as Cash.State).amount}")
@ -299,7 +280,7 @@ class VaultWithCashTest : TestDependencyInjectionBase() {
database.transaction {
// A tx that spends our money.
val spendTXBuilder = TransactionBuilder(DUMMY_NOTARY)
vault.generateSpend(spendTXBuilder, 80.DOLLARS, BOB)
Cash.generateSpend(services, spendTXBuilder, 80.DOLLARS, BOB)
val spendPTX = notaryServices.signInitialTransaction(spendTXBuilder)
val spendTX = services.addSignature(spendPTX, freshKey)
services.recordTransactions(spendTX)