mirror of
https://github.com/corda/corda.git
synced 2025-04-07 11:27:01 +00:00
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:
parent
70495a021e
commit
9958b5c603
@ -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) {
|
||||
|
@ -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)) })
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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")
|
||||
|
@ -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>)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>()
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
Binary file not shown.
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user