Added 'signers' property to the transaction data models. Signers holds the list of all public keys that need to be signed for (command keys and additional ones such as notary).

Removed Notary & ChangeNotary commands, keys to be signed for are added to the signers list during transaction build phase.
This commit is contained in:
Andrius Dagys 2016-06-13 18:56:39 +01:00
parent 70495a021e
commit 9958b5c603
16 changed files with 127 additions and 100 deletions

View File

@ -620,7 +620,6 @@ class InterestRateSwap() : Contract {
}
}
// TODO: pass a notary
override fun generateAgreement(notary: Party): TransactionBuilder = InterestRateSwap().generateAgreement(floatingLeg, fixedLeg, calculation, common, notary)
override fun generateFix(ptx: TransactionBuilder, oldState: StateAndRef<*>, fix: Fix) {

View File

@ -140,7 +140,7 @@ class CommercialPaperTestsGeneric {
}
fun <T : ContractState> cashOutputsToWallet(vararg outputs: TransactionState<T>): Pair<LedgerTransaction, List<StateAndRef<T>>> {
val ltx = LedgerTransaction(emptyList(), emptyList(), listOf(*outputs), emptyList(), SecureHash.randomSHA256(), TransactionType.Business())
val ltx = LedgerTransaction(emptyList(), emptyList(), listOf(*outputs), emptyList(), SecureHash.randomSHA256(), emptyList(), TransactionType.Business())
return Pair(ltx, outputs.mapIndexed { index, state -> StateAndRef(state, StateRef(ltx.id, index)) })
}

View File

@ -203,15 +203,6 @@ data class TimestampCommand(val after: Instant?, val before: Instant?) : Command
val midpoint: Instant get() = after!! + Duration.between(after, before!!).dividedBy(2)
}
/**
* Command that has to be signed by all participants of the states in the transaction
* in order to perform a notary change
*/
class ChangeNotary : TypeOnlyCommandData()
/** Command that indicates the requirement of a Notary signature for the input states */
class NotaryCommand : TypeOnlyCommandData()
/**
* Implemented by a program that implements business logic on the shared ledger. All participants run this code for
* every [LedgerTransaction] they see on the network, for every input and output state. All contracts must accept the

View File

@ -1,9 +1,6 @@
package com.r3corda.core.contracts
import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.signWithECDSA
import com.r3corda.core.crypto.*
import com.r3corda.core.serialization.serialize
import java.security.KeyPair
import java.security.PublicKey
@ -17,11 +14,12 @@ import java.util.*
* Then once the states and commands are right, this class can be used as a holding bucket to gather signatures from
* multiple parties.
*/
class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf(),
private val attachments: MutableList<SecureHash> = arrayListOf(),
private val outputs: MutableList<TransactionState<ContractState>> = arrayListOf(),
private val commands: MutableList<Command> = arrayListOf(),
private val type: TransactionType = TransactionType.Business()) {
open class TransactionBuilder(protected val inputs: MutableList<StateRef> = arrayListOf(),
protected val attachments: MutableList<SecureHash> = arrayListOf(),
protected val outputs: MutableList<TransactionState<ContractState>> = arrayListOf(),
protected val commands: MutableList<Command> = arrayListOf(),
protected val signers: MutableSet<PublicKey> = mutableSetOf(),
protected val type: TransactionType = TransactionType.Business()) {
val time: TimestampCommand? get() = commands.mapNotNull { it.value as? TimestampCommand }.singleOrNull()
@ -57,7 +55,7 @@ class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf
}
/** The signatures that have been collected so far - might be incomplete! */
private val currentSigs = arrayListOf<DigitalSignature.WithKey>()
protected val currentSigs = arrayListOf<DigitalSignature.WithKey>()
fun signWith(key: KeyPair) {
check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" }
@ -94,26 +92,23 @@ class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf
}
fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments),
ArrayList(outputs), ArrayList(commands), type)
ArrayList(outputs), ArrayList(commands), signers.toList(), type)
fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedTransaction {
if (checkSufficientSignatures) {
val gotKeys = currentSigs.map { it.by }.toSet()
for (command in commands) {
if (!gotKeys.containsAll(command.signers))
throw IllegalStateException("Missing signatures on the transaction for a ${command.value.javaClass.canonicalName} command")
}
val missing = signers - gotKeys
if (missing.isNotEmpty())
throw IllegalStateException("Missing signatures on the transaction for the public keys: ${missing.map { it.toStringShort() }}")
}
return SignedTransaction(toWireTransaction().serialize(), ArrayList(currentSigs))
}
fun addInputState(stateAndRef: StateAndRef<*>) {
open fun addInputState(stateAndRef: StateAndRef<*>) {
check(currentSigs.isEmpty())
val notaryKey = stateAndRef.state.notary.owningKey
if (commands.none { it.signers.contains(notaryKey) }) {
commands.add(Command(NotaryCommand(), notaryKey))
}
signers.add(notaryKey)
inputs.add(stateAndRef.ref)
}
@ -130,7 +125,8 @@ class TransactionBuilder(private val inputs: MutableList<StateRef> = arrayListOf
fun addCommand(arg: Command) {
check(currentSigs.isEmpty())
// We should probably merge the lists of pubkeys for identical commands here.
// TODO: replace pubkeys in commands with 'pointers' to keys in signers
signers.addAll(arg.signers)
commands.add(arg)
}

View File

@ -18,7 +18,7 @@ fun WireTransaction.toLedgerTransaction(identityService: IdentityService,
val attachments = attachments.map {
attachmentStorage.openAttachment(it) ?: throw FileNotFoundException(it.toString())
}
return LedgerTransaction(inputs, attachments, outputs, authenticatedArgs, id, type)
return LedgerTransaction(inputs, attachments, outputs, authenticatedArgs, id, signers, type)
}
/**

View File

@ -1,5 +1,8 @@
package com.r3corda.core.contracts
import com.r3corda.core.noneOrSingle
import java.security.PublicKey
/** Defines transaction validation rules for a specific transaction type */
sealed class TransactionType {
override fun equals(other: Any?) = other?.javaClass == javaClass
@ -13,26 +16,43 @@ sealed class TransactionType {
* Note: Presence of _signatures_ is not checked, only the public keys to be signed for.
*/
fun verify(tx: TransactionForVerification) {
verifyNotary(tx)
typeSpecificVerify(tx)
val missing = verifySigners(tx)
if (missing.isNotEmpty()) throw TransactionVerificationException.SignersMissing(tx, missing.toList())
verifyTransaction(tx)
}
private fun verifyNotary(tx: TransactionForVerification) {
if (tx.inStates.isEmpty()) return
val notary = tx.inStates.first().notary
if (tx.inStates.any { it.notary != notary }) throw TransactionVerificationException.MoreThanOneNotary(tx)
if (tx.commands.none { it.signers.contains(notary.owningKey) }) throw TransactionVerificationException.NotaryMissing(tx)
/** Check that the list of signers includes all the necessary keys */
fun verifySigners(tx: TransactionForVerification): Set<PublicKey> {
val timestamp = tx.commands.noneOrSingle { it.value is TimestampCommand }
val timestampKey = timestamp?.signers.orEmpty()
val notaryKey = (tx.inStates.map { it.notary.owningKey } + timestampKey).toSet()
if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx)
val requiredKeys = getRequiredSigners(tx) + notaryKey
val missing = requiredKeys - tx.signers
return missing
}
abstract fun typeSpecificVerify(tx: TransactionForVerification)
/**
* Return the list of public keys that that require signatures for the transaction type.
* Note: the notary key is checked separately for all transactions and need not be included
*/
abstract fun getRequiredSigners(tx: TransactionForVerification): Set<PublicKey>
/** Implement type specific transaction validation logic */
abstract fun verifyTransaction(tx: TransactionForVerification)
/** A general type used for business transactions, where transaction validity is determined by custom contract code */
class Business : TransactionType() {
/**
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
* If any contract fails to verify, the whole transaction is considered to be invalid.
* If any contract fails to verify, the whole transaction is considered to be invalid
*/
override fun typeSpecificVerify(tx: TransactionForVerification) {
override fun verifyTransaction(tx: TransactionForVerification) {
// TODO: Check that notary is unchanged
val ctx = tx.toTransactionForContract()
val contracts = (ctx.inStates.map { it.contract } + ctx.outStates.map { it.contract }).toSet()
@ -44,6 +64,11 @@ sealed class TransactionType {
}
}
}
override fun getRequiredSigners(tx: TransactionForVerification): Set<PublicKey> {
val commandKeys = tx.commands.flatMap { it.signers }.toSet()
return commandKeys
}
}
/**
@ -51,23 +76,36 @@ sealed class TransactionType {
* any contract code, it just checks that the states are unmodified apart from the notary field.
*/
class NotaryChange : TransactionType() {
/**
* A transaction builder that automatically sets the transaction type to [NotaryChange]
* and adds the list of participants to the signers set for every input state.
*/
class Builder() : TransactionBuilder(type = NotaryChange()) {
override fun addInputState(stateAndRef: StateAndRef<*>) {
signers.addAll(stateAndRef.state.data.participants)
super.addInputState(stateAndRef)
}
}
/**
* Check that the difference between inputs and outputs is only the notary field,
* and that all required signing public keys are present
*/
override fun typeSpecificVerify(tx: TransactionForVerification) {
override fun verifyTransaction(tx: TransactionForVerification) {
try {
tx.inStates.zip(tx.outStates).forEach {
check(it.first.data == it.second.data)
check(it.first.notary != it.second.notary)
}
val command = tx.commands.requireSingleCommand<ChangeNotary>()
val requiredSigners = tx.inStates.flatMap { it.data.participants }
check(command.signers.containsAll(requiredSigners))
check(tx.commands.isEmpty())
} catch (e: IllegalStateException) {
throw TransactionVerificationException.InvalidNotaryChange(tx)
}
}
}
}
override fun getRequiredSigners(tx: TransactionForVerification): Set<PublicKey> {
val participantKeys = tx.inStates.flatMap { it.data.participants }.toSet()
return participantKeys
}
}
}

View File

@ -2,6 +2,7 @@ package com.r3corda.core.contracts
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import java.security.PublicKey
import java.util.*
// TODO: Consider moving this out of the core module and providing a different way for unit tests to test contracts.
@ -41,7 +42,7 @@ class TransactionGroup(val transactions: Set<LedgerTransaction>, val nonVerified
// Look up the output in that transaction by index.
inputs.add(ltx.outputs[ref.index])
}
resolved.add(TransactionForVerification(inputs, tx.outputs, tx.attachments, tx.commands, tx.id, tx.type))
resolved.add(TransactionForVerification(inputs, tx.outputs, tx.attachments, tx.commands, tx.id, tx.signers, tx.type))
}
for (tx in resolved)
@ -56,6 +57,7 @@ data class TransactionForVerification(val inStates: List<TransactionState<Contra
val attachments: List<Attachment>,
val commands: List<AuthenticatedObject<CommandData>>,
val origHash: SecureHash,
val signers: List<PublicKey>,
val type: TransactionType) {
override fun hashCode() = origHash.hashCode()
override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash
@ -162,6 +164,6 @@ class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTra
sealed class TransactionVerificationException(val tx: TransactionForVerification, cause: Throwable?) : Exception(cause) {
class ContractRejection(tx: TransactionForVerification, val contract: Contract, cause: Throwable?) : TransactionVerificationException(tx, cause)
class MoreThanOneNotary(tx: TransactionForVerification) : TransactionVerificationException(tx, null)
class NotaryMissing(tx: TransactionForVerification) : TransactionVerificationException(tx, null)
class SignersMissing(tx: TransactionForVerification, missing: List<PublicKey>) : TransactionVerificationException(tx, null)
class InvalidNotaryChange(tx: TransactionForVerification) : TransactionVerificationException(tx, null)
}

View File

@ -45,6 +45,7 @@ data class WireTransaction(val inputs: List<StateRef>,
val attachments: List<SecureHash>,
val outputs: List<TransactionState<ContractState>>,
val commands: List<Command>,
val signers: List<PublicKey>,
val type: TransactionType) : NamedByHash {
// Cache the serialised form of the transaction and its hash to give us fast access to it.
@ -110,7 +111,7 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
/**
* Verify the signatures, deserialise the wire transaction and then check that the set of signatures found contains
* the set of pubkeys in the commands. If any signatures are missing, either throws an exception (by default) or
* the set of pubkeys in the signers list. If any signatures are missing, either throws an exception (by default) or
* returns the list of keys that have missing signatures, depending on the parameter.
*
* @throws SignatureException if a signature is invalid, does not match or if any signature is missing.
@ -141,11 +142,10 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
operator fun plus(sigList: Collection<DigitalSignature.WithKey>) = withAdditionalSignatures(sigList)
/**
* Returns the set of missing signatures - a signature must be present for every command pub key
* and the Notary (if it is specified)
* Returns the set of missing signatures - a signature must be present for each signer public key
*/
fun getMissingSignatures(): Set<PublicKey> {
val requiredKeys = tx.commands.flatMap { it.signers }.toSet()
val requiredKeys = tx.signers.toSet()
val sigKeys = sigs.map { it.by }.toSet()
if (sigKeys.containsAll(requiredKeys)) return emptySet()
@ -171,6 +171,7 @@ data class LedgerTransaction(
val commands: List<AuthenticatedObject<CommandData>>,
/** The hash of the original serialised WireTransaction */
override val id: SecureHash,
val signers: List<PublicKey>,
val type: TransactionType
) : NamedByHash {
@Suppress("UNCHECKED_CAST")

View File

@ -1,8 +1,7 @@
package com.r3corda.core.node.services
import com.r3corda.core.crypto.Party
import com.r3corda.core.contracts.StateRef
import com.r3corda.core.contracts.WireTransaction
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
/**
@ -11,7 +10,7 @@ import com.r3corda.core.crypto.SecureHash
*/
interface UniquenessProvider {
/** Commits all input states of the given transaction */
fun commit(tx: WireTransaction, callerIdentity: Party)
fun commit(states: List<StateRef>, txId: SecureHash, callerIdentity: Party)
/** Specifies the consuming transaction for every conflicting state */
data class Conflict(val stateHistory: Map<StateRef, ConsumingTx>)

View File

@ -29,6 +29,7 @@ import java.io.ObjectOutputStream
import java.lang.reflect.InvocationTargetException
import java.nio.file.Files
import java.nio.file.Path
import java.security.PublicKey
import java.time.Instant
import java.util.*
import javax.annotation.concurrent.ThreadSafe
@ -232,6 +233,7 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
kryo.writeClassAndObject(output, obj.attachments)
kryo.writeClassAndObject(output, obj.outputs)
kryo.writeClassAndObject(output, obj.commands)
kryo.writeClassAndObject(output, obj.signers)
kryo.writeClassAndObject(output, obj.type)
}
@ -261,9 +263,10 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
kryo.useClassLoader(classLoader) {
val outputs = kryo.readClassAndObject(input) as List<TransactionState<ContractState>>
val commands = kryo.readClassAndObject(input) as List<Command>
val signers = kryo.readClassAndObject(input) as List<PublicKey>
val transactionType = kryo.readClassAndObject(input) as TransactionType
return WireTransaction(inputs, attachmentHashes, outputs, commands, transactionType)
return WireTransaction(inputs, attachmentHashes, outputs, commands, signers, transactionType)
}
}
}

View File

@ -108,12 +108,9 @@ abstract class AbstractTransactionForTest {
protected val attachments = ArrayList<SecureHash>()
protected val outStates = ArrayList<LabeledOutput>()
protected val commands = ArrayList<Command>()
protected val signers = LinkedHashSet<PublicKey>()
protected val type = TransactionType.Business()
init {
arg(DUMMY_NOTARY.owningKey) { NotaryCommand() }
}
open fun output(label: String? = null, s: () -> ContractState) = LabeledOutput(label, TransactionState(s(), DUMMY_NOTARY)).apply { outStates.add(this) }
protected fun commandsToAuthenticatedObjects(): List<AuthenticatedObject<CommandData>> {
@ -126,7 +123,7 @@ abstract class AbstractTransactionForTest {
fun arg(vararg key: PublicKey, c: () -> CommandData) {
val keys = listOf(*key)
commands.add(Command(c(), keys))
addCommand(Command(c(), keys))
}
fun timestamp(time: Instant) {
@ -135,7 +132,12 @@ abstract class AbstractTransactionForTest {
}
fun timestamp(data: TimestampCommand) {
commands.add(Command(data, DUMMY_NOTARY.owningKey))
addCommand(Command(data, DUMMY_NOTARY.owningKey))
}
fun addCommand(cmd: Command) {
signers.addAll(cmd.signers)
commands.add(cmd)
}
// Forbid patterns like: transaction { ... transaction { ... } }
@ -156,11 +158,14 @@ sealed class LastLineShouldTestForAcceptOrFailure {
// Corresponds to the args to Contract.verify
open class TransactionForTest : AbstractTransactionForTest() {
private val inStates = arrayListOf<TransactionState<ContractState>>()
fun input(s: () -> ContractState) = inStates.add(TransactionState(s(), DUMMY_NOTARY))
fun input(s: () -> ContractState) {
signers.add(DUMMY_NOTARY.owningKey)
inStates.add(TransactionState(s(), DUMMY_NOTARY))
}
protected fun runCommandsAndVerify(time: Instant) {
val cmds = commandsToAuthenticatedObjects()
val tx = TransactionForVerification(inStates, outStates.map { it.state }, emptyList(), cmds, SecureHash.Companion.randomSHA256(), type)
val tx = TransactionForVerification(inStates, outStates.map { it.state }, emptyList(), cmds, SecureHash.Companion.randomSHA256(), signers.toList(), type)
tx.verify()
}
@ -215,6 +220,9 @@ open class TransactionForTest : AbstractTransactionForTest() {
tx.inStates.addAll(inStates)
tx.outStates.addAll(outStates)
tx.commands.addAll(commands)
tx.signers.addAll(tx.inStates.map { it.notary.owningKey })
tx.signers.addAll(commands.flatMap { it.signers })
return tx.body()
}
@ -246,11 +254,11 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
fun input(label: String) {
val notaryKey = label.output.notary.owningKey
if (commands.none { it.signers.contains(notaryKey) }) commands.add(Command(NotaryCommand(), notaryKey))
signers.add(notaryKey)
inStates.add(label.outputRef)
}
fun toWireTransaction() = WireTransaction(inStates, attachments, outStates.map { it.state }, commands, type)
fun toWireTransaction() = WireTransaction(inStates, attachments, outStates.map { it.state }, commands, signers.toList(), type)
}
val String.output: TransactionState<T>
@ -290,7 +298,7 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
inner class Roots {
fun transaction(vararg outputStates: LabeledOutput) {
val outs = outputStates.map { it.state }
val wtx = WireTransaction(emptyList(), emptyList(), outs, emptyList(), TransactionType.Business())
val wtx = WireTransaction(emptyList(), emptyList(), outs, emptyList(), emptyList(), TransactionType.Business())
for ((index, state) in outputStates.withIndex()) {
val label = state.label!!
labelToRefs[label] = StateRef(wtx.id, index)
@ -370,7 +378,7 @@ class TransactionGroupDSL<T : ContractState>(private val stateType: Class<T>) {
fun signAll(txnsToSign: List<WireTransaction> = txns, vararg extraKeys: KeyPair): List<SignedTransaction> {
return txnsToSign.map { wtx ->
val allPubKeys = wtx.commands.flatMap { it.signers }.toMutableSet()
val allPubKeys = wtx.signers.toMutableSet()
val bits = wtx.serialize()
require(bits == wtx.serialized)
val sigs = ArrayList<DigitalSignature.WithKey>()

View File

@ -163,7 +163,7 @@ object NotaryProtocol {
private fun commitInputStates(tx: WireTransaction, reqIdentity: Party) {
try {
uniquenessProvider.commit(tx, reqIdentity)
uniquenessProvider.commit(tx.inputs, tx.id, reqIdentity)
} catch (e: UniquenessException) {
val conflictData = e.error.serialize()
val signedConflict = SignedData(conflictData, sign(conflictData))

View File

@ -73,8 +73,7 @@ object NotaryChangeProtocol {
val state = originalState.state
val newState = state.withNewNotary(newNotary)
val participants = state.data.participants
val cmd = Command(ChangeNotary(), participants)
val tx = TransactionBuilder(type = TransactionType.NotaryChange()).withItems(originalState, newState, cmd)
val tx = TransactionType.NotaryChange.Builder().withItems(originalState, newState)
tx.signWith(serviceHub.storageService.myLegalIdentityKey)
val stx = tx.toSignedTransaction(false)
@ -161,18 +160,16 @@ object NotaryChangeProtocol {
@Suspendable
private fun validateTx(stx: SignedTransaction): SignedTransaction {
checkMySignatureRequired(stx.tx)
checkDependenciesValid(stx)
checkValid(stx)
checkCommand(stx.tx)
return stx
}
private fun checkCommand(tx: WireTransaction) {
val command = tx.commands.single { it.value is ChangeNotary }
val myKey = serviceHub.storageService.myLegalIdentityKey.public
val myIdentity = serviceHub.storageService.myLegalIdentity
val state = tx.inputs.first()
require(command.signers.contains(myKey)) { "Party $myIdentity is not a participant for the state: $state" }
private fun checkMySignatureRequired(tx: WireTransaction) {
// TODO: use keys from the keyManagementService instead
val myKey = serviceHub.storageService.myLegalIdentity.owningKey
require(tx.signers.contains(myKey)) { "Party is not a participant for any of the input states of transaction ${tx.id}" }
}
@Suspendable

View File

@ -2,8 +2,8 @@ package com.r3corda.node.services.transactions
import com.r3corda.core.ThreadBox
import com.r3corda.core.contracts.StateRef
import com.r3corda.core.contracts.WireTransaction
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.services.UniquenessException
import com.r3corda.core.node.services.UniquenessProvider
import java.util.*
@ -15,12 +15,10 @@ class InMemoryUniquenessProvider() : UniquenessProvider {
/** For each input state store the consuming transaction information */
private val committedStates = ThreadBox(HashMap<StateRef, UniquenessProvider.ConsumingTx>())
// TODO: the uniqueness provider shouldn't be able to see all tx outputs and commands
override fun commit(tx: WireTransaction, callerIdentity: Party) {
val inputStates = tx.inputs
override fun commit(states: List<StateRef>, txId: SecureHash, callerIdentity: Party) {
committedStates.locked {
val conflictingStates = LinkedHashMap<StateRef, UniquenessProvider.ConsumingTx>()
for (inputState in inputStates) {
for (inputState in states) {
val consumingTx = get(inputState)
if (consumingTx != null) conflictingStates[inputState] = consumingTx
}
@ -28,8 +26,8 @@ class InMemoryUniquenessProvider() : UniquenessProvider {
val conflict = UniquenessProvider.Conflict(conflictingStates)
throw UniquenessException(conflict)
} else {
inputStates.forEachIndexed { i, stateRef ->
put(stateRef, UniquenessProvider.ConsumingTx(tx.id, i, callerIdentity))
states.forEachIndexed { i, stateRef ->
put(stateRef, UniquenessProvider.ConsumingTx(txId, i, callerIdentity))
}
}

View File

@ -1,8 +1,6 @@
package com.r3corda.node.services
import com.r3corda.core.contracts.StateRef
import com.r3corda.core.contracts.TransactionType
import com.r3corda.core.contracts.WireTransaction
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.services.UniquenessException
import com.r3corda.core.testing.MEGA_CORP
import com.r3corda.core.testing.generateStateRef
@ -13,30 +11,27 @@ import kotlin.test.assertFailsWith
class UniquenessProviderTests {
val identity = MEGA_CORP
val txID = SecureHash.randomSHA256()
@Test fun `should commit a transaction with unused inputs without exception`() {
val provider = InMemoryUniquenessProvider()
val inputState = generateStateRef()
val tx = buildTransaction(inputState)
provider.commit(tx, identity)
provider.commit(listOf(inputState), txID, identity)
}
@Test fun `should report a conflict for a transaction with previously used inputs`() {
val provider = InMemoryUniquenessProvider()
val inputState = generateStateRef()
val tx1 = buildTransaction(inputState)
provider.commit(tx1, identity)
val inputs = listOf(inputState)
provider.commit(inputs, txID, identity)
val tx2 = buildTransaction(inputState)
val ex = assertFailsWith<UniquenessException> { provider.commit(tx2, identity) }
val ex = assertFailsWith<UniquenessException> { provider.commit(inputs, txID, identity) }
val consumingTx = ex.error.stateHistory[inputState]!!
assertEquals(consumingTx.id, tx1.id)
assertEquals(consumingTx.inputIndex, tx1.inputs.indexOf(inputState))
assertEquals(consumingTx.id, txID)
assertEquals(consumingTx.inputIndex, inputs.indexOf(inputState))
assertEquals(consumingTx.requestingParty, identity)
}
private fun buildTransaction(inputState: StateRef) = WireTransaction(listOf(inputState), emptyList(), emptyList(), emptyList(), TransactionType.Business())
}