Refactor notary change mechanism (#1019)

* Introduce new NotaryChangeWireTransaction (similar to WireTransaction and NotaryChangeTransaction (similar to LedgerTransaction) types.

Remove 'mustSign' and 'signers' fields from transactions

Deprecate the TransactionType concept. When receiving a SignedTransaction, the verification and signature checking branches out for regular and notary change transactions.

Add custom handling logic for notary change transactions to the vault
This commit is contained in:
Andrius Dagys 2017-07-27 08:32:33 +01:00 committed by GitHub
parent 25be649f7b
commit 4487408526
42 changed files with 764 additions and 724 deletions

View File

@ -2,175 +2,16 @@ package net.corda.core.contracts
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.toNonEmptySet
import java.security.PublicKey
/** Defines transaction build & validation logic for a specific transaction type */
@CordaSerializable
// TODO: remove this concept
sealed class TransactionType {
/**
* Check that the transaction is valid based on:
* - General platform rules
* - Rules for the specific transaction type
*
* Note: Presence of _signatures_ is not checked, only the public keys to be signed for.
*/
@Throws(TransactionVerificationException::class)
fun verify(tx: LedgerTransaction) {
require(tx.notary != null || tx.timeWindow == null) { "Transactions with time-windows must be notarised" }
val duplicates = detectDuplicateInputs(tx)
if (duplicates.isNotEmpty()) throw TransactionVerificationException.DuplicateInputStates(tx.id, duplicates.toNonEmptySet())
val missing = verifySigners(tx)
if (missing.isNotEmpty()) throw TransactionVerificationException.SignersMissing(tx.id, missing.toList())
verifyTransaction(tx)
}
/** Check that the list of signers includes all the necessary keys */
fun verifySigners(tx: LedgerTransaction): Set<PublicKey> {
val notaryKey = tx.inputs.map { it.state.notary.owningKey }.toSet()
if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx.id)
val requiredKeys = getRequiredSigners(tx) + notaryKey
val missing = requiredKeys - tx.mustSign
return missing
}
/** Check that the inputs are unique. */
private fun detectDuplicateInputs(tx: LedgerTransaction): Set<StateRef> {
var seenInputs = emptySet<StateRef>()
var duplicates = emptySet<StateRef>()
tx.inputs.forEach { state ->
if (seenInputs.contains(state.ref)) {
duplicates += state.ref
}
seenInputs += state.ref
}
return duplicates
}
/**
* Return the set of public keys 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: LedgerTransaction): Set<PublicKey>
/** Implement type specific transaction validation logic */
abstract fun verifyTransaction(tx: LedgerTransaction)
/** A general transaction type where transaction validity is determined by custom contract code */
object General : TransactionType() {
/** Just uses the default [TransactionBuilder] with no special logic */
@Deprecated("Use TransactionBuilder directly instead", ReplaceWith("TransactionBuilder()"))
class Builder(notary: Party?) : TransactionBuilder(General, notary)
override fun verifyTransaction(tx: LedgerTransaction) {
verifyNoNotaryChange(tx)
verifyEncumbrances(tx)
verifyContracts(tx)
}
/**
* Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there
* are any inputs, all outputs must have the same notary.
*
* TODO: Is that the correct set of restrictions? May need to come back to this, see if we can be more
* flexible on output notaries.
*/
private fun verifyNoNotaryChange(tx: LedgerTransaction) {
if (tx.notary != null && tx.inputs.isNotEmpty()) {
tx.outputs.forEach {
if (it.notary != tx.notary) {
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(tx.id, tx.notary, it.notary)
}
}
}
}
private fun verifyEncumbrances(tx: LedgerTransaction) {
// Validate that all encumbrances exist within the set of input states.
val encumberedInputs = tx.inputs.filter { it.state.encumbrance != null }
encumberedInputs.forEach { (state, ref) ->
val encumbranceStateExists = tx.inputs.any {
it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance
}
if (!encumbranceStateExists) {
throw TransactionVerificationException.TransactionMissingEncumbranceException(
tx.id,
state.encumbrance!!,
TransactionVerificationException.Direction.INPUT
)
}
}
// Check that, in the outputs, an encumbered state does not refer to itself as the encumbrance,
// and that the number of outputs can contain the encumbrance.
for ((i, output) in tx.outputs.withIndex()) {
val encumbranceIndex = output.encumbrance ?: continue
if (encumbranceIndex == i || encumbranceIndex >= tx.outputs.size) {
throw TransactionVerificationException.TransactionMissingEncumbranceException(
tx.id,
encumbranceIndex,
TransactionVerificationException.Direction.OUTPUT)
}
}
}
/**
* 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.
*/
private fun verifyContracts(tx: LedgerTransaction) {
// TODO: This will all be replaced in future once the sandbox and contract constraints work is done.
val contracts = (tx.inputStates.map { it.contract } + tx.outputStates.map { it.contract }).toSet()
for (contract in contracts) {
try {
contract.verify(tx)
} catch(e: Throwable) {
throw TransactionVerificationException.ContractRejection(tx.id, contract, e)
}
}
}
override fun getRequiredSigners(tx: LedgerTransaction) = tx.commands.flatMap { it.signers }.toSet()
}
/**
* A special transaction type for reassigning a notary for a state. Validation does not involve running
* any contract code, it just checks that the states are unmodified apart from the notary field.
*/
object 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(notary: Party) : TransactionBuilder(NotaryChange, notary) {
override fun addInputState(stateAndRef: StateAndRef<*>): TransactionBuilder {
signers.addAll(stateAndRef.state.data.participants.map { it.owningKey })
super.addInputState(stateAndRef)
return this
}
}
/**
* Check that the difference between inputs and outputs is only the notary field, and that all required signing
* public keys are present.
*
* @throws TransactionVerificationException.InvalidNotaryChange if the validity check fails.
*/
override fun verifyTransaction(tx: LedgerTransaction) {
try {
for ((input, output) in tx.inputs.zip(tx.outputs)) {
check(input.state.data == output.data)
check(input.state.notary != output.notary)
}
check(tx.commands.isEmpty())
} catch (e: IllegalStateException) {
throw TransactionVerificationException.InvalidNotaryChange(tx.id)
}
}
override fun getRequiredSigners(tx: LedgerTransaction) = tx.inputStates.flatMap { it.participants }.map { it.owningKey }.toSet()
}
}

View File

@ -23,8 +23,10 @@ sealed class MerkleTree {
/**
* Merkle tree building using hashes, with zero hash padding to full power of 2.
*/
@Throws(IllegalArgumentException::class)
@Throws(MerkleTreeException::class)
fun getMerkleTree(allLeavesHashes: List<SecureHash>): MerkleTree {
if (allLeavesHashes.isEmpty())
throw MerkleTreeException("Cannot calculate Merkle root on empty hash list.")
val leaves = padWithZeros(allLeavesHashes).map { Leaf(it) }
return buildMerkleTree(leaves)
}
@ -46,8 +48,6 @@ sealed class MerkleTree {
* @return Tree root.
*/
private tailrec fun buildMerkleTree(lastNodesList: List<MerkleTree>): MerkleTree {
if (lastNodesList.isEmpty())
throw MerkleTreeException("Cannot calculate Merkle root on empty hash list.")
if (lastNodesList.size == 1) {
return lastNodesList[0] //Root reached.
} else {

View File

@ -5,9 +5,7 @@ import net.corda.core.serialization.CordaSerializable
import java.util.*
@CordaSerializable
class MerkleTreeException(val reason: String) : Exception() {
override fun toString() = "Partial Merkle Tree exception. Reason: $reason"
}
class MerkleTreeException(val reason: String) : Exception("Partial Merkle Tree exception. Reason: $reason")
/**
* Building and verification of Partial Merkle Tree.

View File

@ -9,7 +9,6 @@ import net.corda.core.crypto.isFulfilledBy
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.UntrustworthyData
import net.corda.core.utilities.unwrap
@ -75,7 +74,16 @@ abstract class AbstractStateReplacementFlow {
val finalTx = stx + signatures
serviceHub.recordTransactions(finalTx)
return finalTx.tx.outRef(0)
val newOutput = run {
if (stx.isNotaryChangeTransaction()) {
stx.resolveNotaryChangeTransaction(serviceHub).outRef<T>(0)
} else {
stx.tx.outRef<T>(0)
}
}
return newOutput
}
/**
@ -153,11 +161,10 @@ abstract class AbstractStateReplacementFlow {
@Suspendable
private fun verifyTx(stx: SignedTransaction) {
checkMySignatureRequired(stx.tx)
checkMySignatureRequired(stx)
checkDependenciesValid(stx)
// We expect stx to have insufficient signatures, so we convert the WireTransaction to the LedgerTransaction
// here, thus bypassing the sufficient-signatures check.
stx.tx.toLedgerTransaction(serviceHub).verify()
// We expect stx to have insufficient signatures here
stx.verify(serviceHub, checkSufficientSignatures = false)
}
@Suspendable
@ -174,7 +181,11 @@ abstract class AbstractStateReplacementFlow {
}
val finalTx = stx + allSignatures
if (finalTx.isNotaryChangeTransaction()) {
finalTx.resolveNotaryChangeTransaction(serviceHub).verifyRequiredSignatures()
} else {
finalTx.verifyRequiredSignatures()
}
serviceHub.recordTransactions(finalTx)
}
@ -186,15 +197,22 @@ abstract class AbstractStateReplacementFlow {
@Throws(StateReplacementException::class)
abstract protected fun verifyProposal(proposal: Proposal<T>)
private fun checkMySignatureRequired(tx: WireTransaction) {
private fun checkMySignatureRequired(stx: SignedTransaction) {
// TODO: use keys from the keyManagementService instead
val myKey = serviceHub.myInfo.legalIdentity.owningKey
require(myKey in tx.mustSign) { "Party is not a participant for any of the input states of transaction ${tx.id}" }
val requiredKeys = if (stx.isNotaryChangeTransaction()) {
stx.resolveNotaryChangeTransaction(serviceHub).requiredSigningKeys
} else {
stx.tx.requiredSigningKeys
}
require(myKey in requiredKeys) { "Party is not a participant for any of the input states of transaction ${stx.id}" }
}
@Suspendable
private fun checkDependenciesValid(stx: SignedTransaction) {
subFlow(ResolveTransactionsFlow(stx.tx, otherSide))
subFlow(ResolveTransactionsFlow(stx, otherSide))
}
private fun sign(stx: SignedTransaction): DigitalSignature.WithKey {

View File

@ -17,7 +17,7 @@ import java.security.PublicKey
*
* You would typically use this flow after you have built a transaction with the TransactionBuilder and signed it with
* your key pair. If there are additional signatures to collect then they can be collected using this flow. Signatures
* are collected based upon the [WireTransaction.mustSign] property which contains the union of all the PublicKeys
* are collected based upon the [WireTransaction.requiredSigningKeys] property which contains the union of all the PublicKeys
* listed in the transaction's commands as well as a notary's public key, if required. This flow returns a
* [SignedTransaction] which can then be passed to the [FinalityFlow] for notarisation. The other side of this flow is
* the [SignTransactionFlow].
@ -78,7 +78,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
// Usually just the Initiator and possibly an oracle would have signed at this point.
val myKey = serviceHub.myInfo.legalIdentity.owningKey
val signed = partiallySignedTx.sigs.map { it.by }
val notSigned = partiallySignedTx.tx.mustSign - signed
val notSigned = partiallySignedTx.tx.requiredSigningKeys - signed
// One of the signatures collected so far MUST be from the initiator of this flow.
require(partiallySignedTx.sigs.any { it.by == myKey }) {
@ -113,7 +113,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
/**
* Lookup the [Party] object for each [PublicKey] using the [ServiceHub.networkMapCache].
*/
@Suspendable private fun keysToParties(keys: List<PublicKey>): List<Party> = keys.map {
@Suspendable private fun keysToParties(keys: Collection<PublicKey>): List<Party> = keys.map {
// TODO: Revisit when IdentityService supports resolution of a (possibly random) public key to a legal identity key.
val partyNode = serviceHub.networkMapCache.getNodeByLegalIdentityKey(it)
?: throw IllegalStateException("Party ${it.toBase58String()} not found on the network.")
@ -192,7 +192,7 @@ abstract class SignTransactionFlow(val otherParty: Party,
// Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's.
checkSignatures(proposal)
// Resolve dependencies and verify, pass in the WireTransaction as we don't have all signatures.
subFlow(ResolveTransactionsFlow(proposal.tx, otherParty))
subFlow(ResolveTransactionsFlow(proposal, otherParty))
proposal.tx.toLedgerTransaction(serviceHub).verify()
// Perform some custom verification over the transaction.
try {
@ -221,7 +221,7 @@ abstract class SignTransactionFlow(val otherParty: Party,
"The Initiator of CollectSignaturesFlow must have signed the transaction."
}
val signed = stx.sigs.map { it.by }
val allSigners = stx.tx.mustSign
val allSigners = stx.tx.requiredSigningKeys
val notSigned = allSigners - signed
stx.verifySignaturesExcept(*notSigned.toTypedArray())
}
@ -251,7 +251,7 @@ abstract class SignTransactionFlow(val otherParty: Party,
@Suspendable private fun checkMySignatureRequired(stx: SignedTransaction) {
// TODO: Revisit when key management is properly fleshed out.
val myKey = serviceHub.myInfo.legalIdentity.owningKey
require(myKey in stx.tx.mustSign) {
require(myKey in stx.tx.requiredSigningKeys) {
"Party is not a participant for any of the input states of transaction ${stx.id}"
}
}

View File

@ -159,8 +159,8 @@ open class FinalityFlow(val transactions: Iterable<SignedTransaction>,
return sorted.map { stx ->
val notary = stx.tx.notary
// The notary signature(s) are allowed to be missing but no others.
val wtx = if (notary != null) stx.verifySignaturesExcept(notary.owningKey) else stx.verifyRequiredSignatures()
val ltx = wtx.toLedgerTransaction(augmentedLookup)
if (notary != null) stx.verifySignaturesExcept(notary.owningKey) else stx.verifyRequiredSignatures()
val ltx = stx.toLedgerTransaction(augmentedLookup, false)
ltx.verify()
stx to ltx
}

View File

@ -1,9 +1,9 @@
package net.corda.core.flows
import net.corda.core.contracts.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker
/**
@ -23,61 +23,31 @@ class NotaryChangeFlow<out T : ContractState>(
: AbstractStateReplacementFlow.Instigator<T, T, Party>(originalState, newNotary, progressTracker) {
override fun assembleTx(): AbstractStateReplacementFlow.UpgradeTx {
val state = originalState.state
val tx = TransactionType.NotaryChange.Builder(originalState.state.notary)
val inputs = resolveEncumbrances(originalState)
val participants: Iterable<AbstractParty> = if (state.encumbrance == null) {
val modifiedState = TransactionState(state.data, modification)
tx.addInputState(originalState)
tx.addOutputState(modifiedState)
state.data.participants
} else {
resolveEncumbrances(tx)
}
val tx = NotaryChangeWireTransaction(
inputs.map { it.ref },
originalState.state.notary,
modification
)
val stx = serviceHub.signInitialTransaction(tx)
val participantKeys = participants.map { it.owningKey }
val participantKeys = inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet()
// TODO: We need a much faster way of finding our key in the transaction
val myKey = serviceHub.keyManagementService.filterMyKeys(participantKeys).single()
val mySignature = serviceHub.keyManagementService.sign(tx.id.bytes, myKey)
val stx = SignedTransaction(tx, listOf(mySignature))
return AbstractStateReplacementFlow.UpgradeTx(stx, participantKeys, myKey)
}
/**
* Adds the notary change state transitions to the [tx] builder for the [originalState] and its encumbrance
* state chain (encumbrance states might be themselves encumbered by other states).
*
* @return union of all added states' participants
*/
private fun resolveEncumbrances(tx: TransactionBuilder): Iterable<AbstractParty> {
val stateRef = originalState.ref
val txId = stateRef.txhash
val issuingTx = serviceHub.validatedTransactions.getTransaction(txId)
?: throw StateReplacementException("Transaction $txId not found")
val outputs = issuingTx.tx.outputs
val participants = mutableSetOf<AbstractParty>()
var nextStateIndex = stateRef.index
var newOutputPosition = tx.outputStates().size
while (true) {
val nextState = outputs[nextStateIndex]
tx.addInputState(StateAndRef(nextState, StateRef(txId, nextStateIndex)))
participants.addAll(nextState.data.participants)
if (nextState.encumbrance == null) {
val modifiedState = TransactionState(nextState.data, modification)
tx.addOutputState(modifiedState)
break
} else {
val modifiedState = TransactionState(nextState.data, modification, newOutputPosition + 1)
tx.addOutputState(modifiedState)
nextStateIndex = nextState.encumbrance
/** Resolves the encumbrance state chain for the given [state] */
private fun resolveEncumbrances(state: StateAndRef<T>): List<StateAndRef<T>> {
val states = mutableListOf(state)
while (states.last().state.encumbrance != null) {
val encumbranceStateRef = StateRef(states.last().ref.txhash, states.last().state.encumbrance!!)
val encumbranceState = serviceHub.toStateAndRef<T>(encumbranceStateRef)
states.add(encumbranceState)
}
newOutputPosition++
}
return participants
return states
}
}

View File

@ -8,7 +8,9 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignedData
import net.corda.core.crypto.keys
import net.corda.core.identity.Party
import net.corda.core.node.services.*
import net.corda.core.node.services.NotaryService
import net.corda.core.node.services.TrustedAuthorityNotaryService
import net.corda.core.node.services.UniquenessProvider
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker
@ -45,13 +47,18 @@ object NotaryFlow {
@Throws(NotaryException::class)
override fun call(): List<DigitalSignature.WithKey> {
progressTracker.currentStep = REQUESTING
val wtx = stx.tx
notaryParty = wtx.notary ?: throw IllegalStateException("Transaction does not specify a Notary")
check(wtx.inputs.all { stateRef -> serviceHub.loadState(stateRef).notary == notaryParty }) {
notaryParty = stx.notary ?: throw IllegalStateException("Transaction does not specify a Notary")
check(stx.inputs.all { stateRef -> serviceHub.loadState(stateRef).notary == notaryParty }) {
"Input states must have the same Notary"
}
try {
if (stx.isNotaryChangeTransaction()) {
stx.resolveNotaryChangeTransaction(serviceHub).verifySignaturesExcept(notaryParty.owningKey)
} else {
stx.verifySignaturesExcept(notaryParty.owningKey)
}
} catch (ex: SignatureException) {
throw NotaryException(NotaryError.TransactionInvalid(ex))
}
@ -59,7 +66,11 @@ object NotaryFlow {
val payload: Any = if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
stx
} else {
wtx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow })
if (stx.isNotaryChangeTransaction()) {
stx.notaryChangeTx
} else {
stx.tx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow })
}
}
val response = try {

View File

@ -3,36 +3,31 @@ package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.utilities.exactAdd
import net.corda.core.crypto.SecureHash
import net.corda.core.getOrThrow
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import java.util.*
// TODO: This code is currently unit tested by TwoPartyTradeFlowTests, it should have its own tests.
// TODO: It may be a clearer API if we make the primary c'tor private here, and only allow a single tx to be "resolved".
/**
* This flow is used to verify the validity of a transaction by recursively checking the validity of all the
* dependencies. Once a transaction is checked it's inserted into local storage so it can be relayed and won't be
* checked again.
* Resolves transactions for the specified [txHashes] along with their full history (dependency graph) from [otherSide].
* Each retrieved transaction is validated and inserted into the local transaction storage.
*
* A couple of constructors are provided that accept a single transaction. When these are used, the dependencies of that
* transaction are resolved and then the transaction itself is verified. Again, if successful, the results are inserted
* into the database as long as a [SignedTransaction] was provided. If only the [WireTransaction] form was provided
* then this isn't enough to put into the local database, so only the dependencies are checked and inserted. This way
* to use the flow is helpful when resolving and verifying a finished but partially signed transaction.
*
* The flow returns a list of verified [LedgerTransaction] objects, in a depth-first order.
* @return a list of verified [SignedTransaction] objects, in a depth-first order.
*/
class ResolveTransactionsFlow(private val txHashes: Set<SecureHash>,
private val otherSide: Party) : FlowLogic<List<LedgerTransaction>>() {
private val otherSide: Party) : FlowLogic<List<SignedTransaction>>() {
/**
* Resolves and validates the dependencies of the specified [signedTransaction]. Fetches the attachments, but does
* *not* validate or store the [signedTransaction] itself.
*
* @return a list of verified [SignedTransaction] objects, in a depth-first order.
*/
constructor(signedTransaction: SignedTransaction, otherSide: Party) : this(dependencyIDs(signedTransaction), otherSide) {
this.signedTransaction = signedTransaction
}
companion object {
private fun dependencyIDs(wtx: WireTransaction) = wtx.inputs.map { it.txhash }.toSet()
private fun dependencyIDs(stx: SignedTransaction) = stx.inputs.map { it.txhash }.toSet()
/**
* Topologically sorts the given transactions such that dependencies are listed before dependers. */
@ -41,7 +36,7 @@ class ResolveTransactionsFlow(private val txHashes: Set<SecureHash>,
// Construct txhash -> dependent-txs map
val forwardGraph = HashMap<SecureHash, HashSet<SignedTransaction>>()
transactions.forEach { stx ->
stx.tx.inputs.forEach { (txhash) ->
stx.inputs.forEach { (txhash) ->
// Note that we use a LinkedHashSet here to make the traversal deterministic (as long as the input list is)
forwardGraph.getOrPut(txhash) { LinkedHashSet() }.add(stx)
}
@ -64,67 +59,40 @@ class ResolveTransactionsFlow(private val txHashes: Set<SecureHash>,
require(result.size == transactions.size)
return result
}
}
@CordaSerializable
class ExcessivelyLargeTransactionGraph : Exception()
// Transactions to verify after the dependencies.
private var stx: SignedTransaction? = null
private var wtx: WireTransaction? = null
/** Transaction for fetch attachments for */
private var signedTransaction: SignedTransaction? = null
// TODO: Figure out a more appropriate DOS limit here, 5000 is simply a very bad guess.
/** The maximum number of transactions this flow will try to download before bailing out. */
var transactionCountLimit = 5000
/**
* Resolve the full history of a transaction and verify it with its dependencies.
*/
constructor(stx: SignedTransaction, otherSide: Party) : this(stx.tx, otherSide) {
this.stx = stx
}
/**
* Resolve the full history of a transaction and verify it with its dependencies.
*/
constructor(wtx: WireTransaction, otherSide: Party) : this(dependencyIDs(wtx), otherSide) {
this.wtx = wtx
}
@Suspendable
@Throws(FetchDataFlow.HashNotFound::class)
override fun call(): List<LedgerTransaction> {
override fun call(): List<SignedTransaction> {
val newTxns: Iterable<SignedTransaction> = topologicalSort(downloadDependencies(txHashes))
// For each transaction, verify it and insert it into the database. As we are iterating over them in a
// depth-first order, we should not encounter any verification failures due to missing data. If we fail
// half way through, it's no big deal, although it might result in us attempting to re-download data
// redundantly next time we attempt verification.
val result = ArrayList<LedgerTransaction>()
val result = ArrayList<SignedTransaction>()
for (stx in newTxns) {
// Resolve to a LedgerTransaction and then run all contracts.
val ltx = stx.toLedgerTransaction(serviceHub)
// Block on each verification request.
// TODO We could recover some parallelism from the dependency graph.
serviceHub.transactionVerifierService.verify(ltx).getOrThrow()
// TODO: We could recover some parallelism from the dependency graph.
stx.verify(serviceHub)
serviceHub.recordTransactions(stx)
result += ltx
result += stx
}
// If this flow is resolving a specific transaction, make sure we have its attachments and then verify
// it as well, but don't insert to the database. Note that when we were given a SignedTransaction (stx != null)
// we *could* insert, because successful verification implies we have everything we need here, and it might
// be a clearer API if we do that. But for consistency with the other c'tor we currently do not.
//
// If 'stx' is set, then 'wtx' is the contents (from the c'tor).
val wtx = stx?.verifyRequiredSignatures() ?: wtx
wtx?.let {
// If this flow is resolving a specific transaction, make sure we have its attachments as well
signedTransaction?.let {
fetchMissingAttachments(listOf(it))
val ltx = it.toLedgerTransaction(serviceHub)
ltx.verify()
result += ltx
result += it
}
return result
@ -167,13 +135,13 @@ class ResolveTransactionsFlow(private val txHashes: Set<SecureHash>,
// Request the standalone transaction data (which may refer to things we don't yet have).
val downloads: List<SignedTransaction> = subFlow(FetchTransactionsFlow(notAlreadyFetched, otherSide)).downloaded
fetchMissingAttachments(downloads.map { it.tx })
fetchMissingAttachments(downloads)
for (stx in downloads)
check(resultQ.putIfAbsent(stx.id, stx) == null) // Assert checks the filter at the start.
// Add all input states to the work queue.
val inputHashes = downloads.flatMap { it.tx.inputs }.map { it.txhash }
val inputHashes = downloads.flatMap { it.inputs }.map { it.txhash }
nextRequests.addAll(inputHashes)
limitCounter = limitCounter exactAdd nextRequests.size
@ -189,9 +157,10 @@ class ResolveTransactionsFlow(private val txHashes: Set<SecureHash>,
* first in the returned list and thus doesn't have any unverified dependencies.
*/
@Suspendable
private fun fetchMissingAttachments(downloads: List<WireTransaction>) {
private fun fetchMissingAttachments(downloads: List<SignedTransaction>) {
// TODO: This could be done in parallel with other fetches for extra speed.
val missingAttachments = downloads.flatMap { wtx ->
val wireTransactions = downloads.filterNot { it.isNotaryChangeTransaction() }.map { it.tx }
val missingAttachments = wireTransactions.flatMap { wtx ->
wtx.attachments.filter { serviceHub.attachments.openAttachment(it) == null }
}
if (missingAttachments.isNotEmpty())

View File

@ -87,7 +87,10 @@ interface ServiceHub : ServicesForResolution {
@Throws(TransactionResolutionException::class)
override fun loadState(stateRef: StateRef): TransactionState<*> {
val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
return stx.tx.outputs[stateRef.index]
return if (stx.isNotaryChangeTransaction()) {
stx.resolveNotaryChangeTransaction(this).outputs[stateRef.index]
}
else stx.tx.outputs[stateRef.index]
}
/**
@ -98,7 +101,12 @@ interface ServiceHub : ServicesForResolution {
@Throws(TransactionResolutionException::class)
fun <T : ContractState> toStateAndRef(stateRef: StateRef): StateAndRef<T> {
val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
return stx.tx.outRef<T>(stateRef.index)
return if (stx.isNotaryChangeTransaction()) {
stx.resolveNotaryChangeTransaction(this).outRef<T>(stateRef.index)
}
else {
stx.tx.outRef<T>(stateRef.index)
}
}
/**

View File

@ -10,8 +10,8 @@ import net.corda.core.identity.Party
import net.corda.core.messaging.DataFeed
import net.corda.core.serialization.CordaSerializable
import net.corda.core.toFuture
import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.OpaqueBytes
import rx.Observable
@ -44,7 +44,15 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
* other transactions observed, then the changes are observed "net" of those.
*/
@CordaSerializable
data class Update<U : ContractState>(val consumed: Set<StateAndRef<U>>, val produced: Set<StateAndRef<U>>, val flowId: UUID? = null) {
data class Update<U : ContractState>(val consumed: Set<StateAndRef<U>>,
val produced: Set<StateAndRef<U>>,
val flowId: UUID? = null,
/**
* Specifies the type of update, currently supported types are general and notary change. Notary
* change transactions only modify the notary field on states, and potentially need to be handled
* differently.
*/
val type: UpdateType = UpdateType.GENERAL) {
/** Checks whether the update contains a state of the specified type. */
inline fun <reified T : ContractState> containsType() = consumed.any { it.state.data is T } || produced.any { it.state.data is T }
@ -92,6 +100,11 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
UNCONSUMED, CONSUMED, ALL
}
@CordaSerializable
enum class UpdateType {
GENERAL, NOTARY_CHANGE
}
/**
* Returned in queries [VaultService.queryBy] and [VaultService.trackBy].
* A Page contains:
@ -177,10 +190,10 @@ interface VaultService {
*
* TODO: Consider if there's a good way to enforce the must-be-verified requirement in the type system.
*/
fun notifyAll(txns: Iterable<WireTransaction>)
fun notifyAll(txns: Iterable<CoreTransaction>)
/** Same as notifyAll but with a single transaction. */
fun notify(tx: WireTransaction) = notifyAll(listOf(tx))
fun notify(tx: CoreTransaction) = notifyAll(listOf(tx))
/**
* Provide a [Future] for when a [StateRef] is consumed, which can be very useful in building tests.

View File

@ -14,6 +14,7 @@ import de.javakaffee.kryoserializers.guava.*
import net.corda.core.crypto.MetaData
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.node.CordaPluginRegistry
import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.NonEmptySet
@ -59,6 +60,11 @@ object DefaultKryoCustomizer {
instantiatorStrategy = CustomInstantiatorStrategy()
// WARNING: reordering the registrations here will cause a change in the serialized form, since classes
// with custom serializers get written as registration ids. This will break backwards-compatibility.
// Please add any new registrations to the end.
// TODO: re-organise registrations into logical groups before v1.0
register(Arrays.asList("").javaClass, ArraysAsListSerializer())
register(SignedTransaction::class.java, SignedTransactionSerializer)
register(WireTransaction::class.java, WireTransactionSerializer)
@ -116,6 +122,8 @@ object DefaultKryoCustomizer {
register(BCSphincs256PublicKey::class.java, PublicKeySerializer)
register(sun.security.ec.ECPublicKeyImpl::class.java, PublicKeySerializer)
register(NotaryChangeWireTransaction::class.java, NotaryChangeWireTransactionSerializer)
val customization = KryoSerializationCustomization(this)
pluginRegistries.forEach { it.customizeSerialization(customization) }
}

View File

@ -9,6 +9,8 @@ import net.corda.core.contracts.*
import net.corda.core.crypto.*
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.identity.Party
import net.corda.core.transactions.CoreTransaction
import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.i2p.crypto.eddsa.EdDSAPrivateKey
@ -37,6 +39,7 @@ import kotlin.reflect.KMutableProperty
import kotlin.reflect.KParameter
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaType
/**
@ -112,6 +115,7 @@ class ImmutableClassSerializer<T : Any>(val klass: KClass<T>) : Serializer<T>()
output.writeInt(hashParameters(constructor.parameters))
for (param in constructor.parameters) {
val kProperty = propsByName[param.name!!]!!
kProperty.isAccessible = true
when (param.type.javaType.typeName) {
"int" -> output.writeVarInt(kProperty.get(obj) as Int, true)
"long" -> output.writeVarLong(kProperty.get(obj) as Long, true)
@ -239,7 +243,6 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
kryo.writeClassAndObject(output, obj.outputs)
kryo.writeClassAndObject(output, obj.commands)
kryo.writeClassAndObject(output, obj.notary)
kryo.writeClassAndObject(output, obj.mustSign)
kryo.writeClassAndObject(output, obj.type)
kryo.writeClassAndObject(output, obj.timeWindow)
}
@ -267,14 +270,31 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
val outputs = kryo.readClassAndObject(input) as List<TransactionState<ContractState>>
val commands = kryo.readClassAndObject(input) as List<Command<*>>
val notary = kryo.readClassAndObject(input) as Party?
val signers = kryo.readClassAndObject(input) as List<PublicKey>
val transactionType = kryo.readClassAndObject(input) as TransactionType
val timeWindow = kryo.readClassAndObject(input) as TimeWindow?
return WireTransaction(inputs, attachmentHashes, outputs, commands, notary, signers, transactionType, timeWindow)
return WireTransaction(inputs, attachmentHashes, outputs, commands, notary, transactionType, timeWindow)
}
}
}
@ThreadSafe
object NotaryChangeWireTransactionSerializer : Serializer<NotaryChangeWireTransaction>() {
override fun write(kryo: Kryo, output: Output, obj: NotaryChangeWireTransaction) {
kryo.writeClassAndObject(output, obj.inputs)
kryo.writeClassAndObject(output, obj.notary)
kryo.writeClassAndObject(output, obj.newNotary)
}
@Suppress("UNCHECKED_CAST")
override fun read(kryo: Kryo, input: Input, type: Class<NotaryChangeWireTransaction>): NotaryChangeWireTransaction {
val inputs = kryo.readClassAndObject(input) as List<StateRef>
val notary = kryo.readClassAndObject(input) as Party
val newNotary = kryo.readClassAndObject(input) as Party
return NotaryChangeWireTransaction(inputs, notary, newNotary)
}
}
@ThreadSafe
object SignedTransactionSerializer : Serializer<SignedTransaction>() {
override fun write(kryo: Kryo, output: Output, obj: SignedTransaction) {
@ -285,7 +305,7 @@ object SignedTransactionSerializer : Serializer<SignedTransaction>() {
@Suppress("UNCHECKED_CAST")
override fun read(kryo: Kryo, input: Input, type: Class<SignedTransaction>): SignedTransaction {
return SignedTransaction(
kryo.readClassAndObject(input) as SerializedBytes<WireTransaction>,
kryo.readClassAndObject(input) as SerializedBytes<CoreTransaction>,
kryo.readClassAndObject(input) as List<DigitalSignature.WithKey>
)
}

View File

@ -4,61 +4,38 @@ import net.corda.core.contracts.*
import net.corda.core.identity.Party
import net.corda.core.internal.indexOfOrThrow
import net.corda.core.internal.castIfPossible
import java.security.PublicKey
import java.util.*
import java.util.function.Predicate
/**
* An abstract class defining fields shared by all transaction types in the system.
*/
abstract class BaseTransaction(
abstract class BaseTransaction : NamedByHash {
/** The inputs of this transaction. Note that in BaseTransaction subclasses the type of this list may change! */
open val inputs: List<*>,
abstract val inputs: List<*>
/** Ordered list of states defined by this transaction, along with the associated notaries. */
val outputs: List<TransactionState<ContractState>>,
abstract val outputs: List<TransactionState<ContractState>>
/**
* If present, the notary for this transaction. If absent then the transaction is not notarised at all.
* This is intended for issuance/genesis transactions that don't consume any other states and thus can't
* double spend anything.
*/
val notary: Party?,
/**
* Public keys that need to be fulfilled by signatures in order for the transaction to be valid.
* In a [SignedTransaction] this list is used to check whether there are any missing signatures. Note that
* there is nothing that forces the list to be the _correct_ list of signers for this transaction until
* the transaction is verified by using [LedgerTransaction.verify].
*
* It includes the notary key, if the notary field is set.
*/
val mustSign: List<PublicKey>,
/**
* Pointer to a class that defines the behaviour of this transaction: either normal, or "notary changing".
*/
val type: TransactionType,
/**
* If specified, a time window in which this transaction may have been notarised. Contracts can check this
* time window to find out when a transaction is deemed to have occurred, from the ledger's perspective.
*/
val timeWindow: TimeWindow?
) : NamedByHash {
abstract val notary: Party?
protected fun checkInvariants() {
if (notary == null) check(inputs.isEmpty()) { "The notary must be specified explicitly for any transaction that has inputs" }
if (timeWindow != null) check(notary != null) { "If a time-window is provided, there must be a notary" }
protected open fun checkBaseInvariants() {
checkNotarySetIfInputsPresent()
checkNoDuplicateInputs()
}
override fun equals(other: Any?): Boolean {
if (other === this) return true
return other is BaseTransaction &&
notary == other.notary &&
mustSign == other.mustSign &&
type == other.type &&
timeWindow == other.timeWindow
private fun checkNotarySetIfInputsPresent() {
if (notary == null) {
check(inputs.isEmpty()) { "The notary must be specified explicitly for any transaction that has inputs" }
}
}
override fun hashCode() = Objects.hash(notary, mustSign, type, timeWindow)
override fun toString(): String = "${javaClass.simpleName}(id=$id)"
private fun checkNoDuplicateInputs() {
val duplicates = inputs.groupBy { it }.filter { it.value.size > 1 }.keys
check(duplicates.isEmpty()) { "Duplicate input states detected" }
}
/**
* Returns a [StateAndRef] for the given output index.
@ -173,4 +150,6 @@ abstract class BaseTransaction(
inline fun <reified T : ContractState> findOutRef(crossinline predicate: (T) -> Boolean): StateAndRef<T> {
return findOutRef(T::class.java, Predicate { predicate(it) })
}
override fun toString(): String = "${javaClass.simpleName}(id=$id)"
}

View File

@ -0,0 +1,32 @@
package net.corda.core.transactions
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef
/**
* A transaction with the minimal amount of information required to compute the unique transaction [id], and
* resolve a [FullTransaction]. This type of transaction, wrapped in [SignedTransaction], gets transferred across the
* wire and recorded to storage.
*/
abstract class CoreTransaction : BaseTransaction() {
/** The inputs of this transaction, containing state references only **/
abstract override val inputs: List<StateRef>
}
/** A transaction with fully resolved components, such as input states. */
abstract class FullTransaction : BaseTransaction() {
abstract override val inputs: List<StateAndRef<ContractState>>
override fun checkBaseInvariants() {
super.checkBaseInvariants()
checkInputsHaveSameNotary()
}
private fun checkInputsHaveSameNotary() {
if (inputs.isEmpty()) return
val inputNotaries = inputs.map { it.state.notary }.toHashSet()
check(inputNotaries.size == 1) { "All inputs must point to the same notary" }
check(inputNotaries.single() == notary) { "The specified notary must be the one specified by all inputs" }
}
}

View File

@ -5,7 +5,6 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.internal.castIfPossible
import net.corda.core.serialization.CordaSerializable
import java.security.PublicKey
import java.util.*
import java.util.function.Predicate
@ -24,24 +23,26 @@ import java.util.function.Predicate
// currently sends this across to out-of-process verifiers. We'll need to change that first.
// DOCSTART 1
@CordaSerializable
class LedgerTransaction(
data class LedgerTransaction(
/** The resolved input states which will be consumed/invalidated by the execution of this transaction. */
override val inputs: List<StateAndRef<*>>,
outputs: List<TransactionState<ContractState>>,
override val inputs: List<StateAndRef<ContractState>>,
override val outputs: List<TransactionState<ContractState>>,
/** Arbitrary data passed to the program of each input state. */
val commands: List<AuthenticatedObject<CommandData>>,
/** A list of [Attachment] objects identified by the transaction that are needed for this transaction to verify. */
val attachments: List<Attachment>,
/** The hash of the original serialised WireTransaction. */
override val id: SecureHash,
notary: Party?,
signers: List<PublicKey>,
timeWindow: TimeWindow?,
type: TransactionType
) : BaseTransaction(inputs, outputs, notary, signers, type, timeWindow) {
override val notary: Party?,
val timeWindow: TimeWindow?,
val type: TransactionType
) : FullTransaction() {
//DOCEND 1
init {
checkInvariants()
checkBaseInvariants()
if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" }
checkNoNotaryChange()
checkEncumbrancesValid()
}
val inputStates: List<ContractState> get() = inputs.map { it.state.data }
@ -55,42 +56,72 @@ class LedgerTransaction(
fun <T : ContractState> inRef(index: Int): StateAndRef<T> = inputs[index] as StateAndRef<T>
/**
* Verifies this transaction and throws an exception if not valid, depending on the type. For general transactions:
*
* - The contracts are run with the transaction as the input.
* - The list of keys mentioned in commands is compared against the signers list.
* Verifies this transaction and runs contract code. At this stage it is assumed that signatures have already been verified.
*
* @throws TransactionVerificationException if anything goes wrong.
*/
@Throws(TransactionVerificationException::class)
fun verify() = type.verify(this)
fun verify() = verifyContracts()
// TODO: When we upgrade to Kotlin 1.1 we can make this a data class again and have the compiler generate these.
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
if (!super.equals(other)) return false
other as LedgerTransaction
if (inputs != other.inputs) return false
if (outputs != other.outputs) return false
if (commands != other.commands) return false
if (attachments != other.attachments) return false
if (id != other.id) return false
return true
/**
* 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.
*/
private fun verifyContracts() {
val contracts = (inputs.map { it.state.data.contract } + outputs.map { it.data.contract }).toSet()
for (contract in contracts) {
try {
contract.verify(this)
} catch(e: Throwable) {
throw TransactionVerificationException.ContractRejection(id, contract, e)
}
}
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + inputs.hashCode()
result = 31 * result + outputs.hashCode()
result = 31 * result + commands.hashCode()
result = 31 * result + attachments.hashCode()
result = 31 * result + id.hashCode()
return result
/**
* Make sure the notary has stayed the same. As we can't tell how inputs and outputs connect, if there
* are any inputs, all outputs must have the same notary.
*
* TODO: Is that the correct set of restrictions? May need to come back to this, see if we can be more
* flexible on output notaries.
*/
private fun checkNoNotaryChange() {
if (notary != null && inputs.isNotEmpty()) {
outputs.forEach {
if (it.notary != notary) {
throw TransactionVerificationException.NotaryChangeInWrongTransactionType(id, notary, it.notary)
}
}
}
}
private fun checkEncumbrancesValid() {
// Validate that all encumbrances exist within the set of input states.
val encumberedInputs = inputs.filter { it.state.encumbrance != null }
encumberedInputs.forEach { (state, ref) ->
val encumbranceStateExists = inputs.any {
it.ref.txhash == ref.txhash && it.ref.index == state.encumbrance
}
if (!encumbranceStateExists) {
throw TransactionVerificationException.TransactionMissingEncumbranceException(
id,
state.encumbrance!!,
TransactionVerificationException.Direction.INPUT
)
}
}
// Check that, in the outputs, an encumbered state does not refer to itself as the encumbrance,
// and that the number of outputs can contain the encumbrance.
for ((i, output) in outputs.withIndex()) {
val encumbranceIndex = output.encumbrance ?: continue
if (encumbranceIndex == i || encumbranceIndex >= outputs.size) {
throw TransactionVerificationException.TransactionMissingEncumbranceException(
id,
encumbranceIndex,
TransactionVerificationException.Direction.OUTPUT)
}
}
}
/**

View File

@ -9,7 +9,6 @@ import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializationDefaults.P2P_CONTEXT
import net.corda.core.serialization.serialize
import java.security.PublicKey
import java.util.function.Predicate
fun <T : Any> serializedHash(x: T): SecureHash {
@ -31,7 +30,6 @@ interface TraversableTransaction {
val outputs: List<TransactionState<ContractState>>
val commands: List<Command<*>>
val notary: Party?
val mustSign: List<PublicKey>
val type: TransactionType?
val timeWindow: TimeWindow?
@ -54,7 +52,6 @@ interface TraversableTransaction {
// torn-off transaction and id calculation.
val result = mutableListOf(inputs, attachments, outputs, commands).flatten().toMutableList()
notary?.let { result += it }
result.addAll(mustSign)
type?.let { result += it }
timeWindow?.let { result += it }
return result
@ -79,7 +76,6 @@ class FilteredLeaves(
override val outputs: List<TransactionState<ContractState>>,
override val commands: List<Command<*>>,
override val notary: Party?,
override val mustSign: List<PublicKey>,
override val type: TransactionType?,
override val timeWindow: TimeWindow?
) : TraversableTransaction {

View File

@ -0,0 +1,92 @@
package net.corda.core.transactions
import net.corda.core.contracts.*
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.toBase58String
import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub
import java.security.PublicKey
/**
* A special transaction for changing the notary of a state. It only needs specifying the state(s) as input(s),
* old and new notaries. Output states can be computed by applying the notary modification to corresponding inputs
* on the fly.
*/
data class NotaryChangeWireTransaction(
override val inputs: List<StateRef>,
override val notary: Party,
val newNotary: Party
) : CoreTransaction() {
/**
* This transaction does not contain any output states, outputs can be obtained by resolving a
* [NotaryChangeLedgerTransaction] and applying the notary modification to inputs.
*/
override val outputs: List<TransactionState<ContractState>>
get() = emptyList()
init {
check(inputs.isNotEmpty()) { "A notary change transaction must have inputs" }
check(notary != newNotary) { "The old and new notaries must be different $newNotary" }
}
override val id: SecureHash by lazy { serializedHash(inputs + notary + newNotary) }
fun resolve(services: ServiceHub, sigs: List<DigitalSignature.WithKey>): NotaryChangeLedgerTransaction {
val resolvedInputs = inputs.map { ref ->
services.loadState(ref).let { StateAndRef(it, ref) }
}
return NotaryChangeLedgerTransaction(resolvedInputs, notary, newNotary, id, sigs)
}
}
/**
* A notary change transaction with fully resolved inputs and signatures. In contrast with a regular transaction,
* signatures are checked against the signers specified by input states' *participants* fields, so full resolution is
* needed for signature verification.
*/
data class NotaryChangeLedgerTransaction(
override val inputs: List<StateAndRef<ContractState>>,
override val notary: Party,
val newNotary: Party,
override val id: SecureHash,
override val sigs: List<DigitalSignature.WithKey>
) : FullTransaction(), TransactionWithSignatures {
init {
checkEncumbrances()
}
/** We compute the outputs on demand by applying the notary field modification to the inputs */
override val outputs: List<TransactionState<ContractState>>
get() = inputs.mapIndexed { pos, (state) ->
if (state.encumbrance != null) {
state.copy(notary = newNotary, encumbrance = pos + 1)
} else state.copy(notary = newNotary)
}
override val requiredSigningKeys: Set<PublicKey>
get() = inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet() + notary.owningKey
override fun getKeyDescriptions(keys: Set<PublicKey>): List<String> {
return keys.map { it.toBase58String() }
}
/**
* Check that encumbrances have been included in the inputs. The [NotaryChangeFlow] guarantees that an encumbrance
* will follow its encumbered state in the inputs.
*/
private fun checkEncumbrances() {
inputs.forEachIndexed { i, (state, ref) ->
state.encumbrance?.let {
val nextIndex = i + 1
fun nextStateIsEncumbrance() = (inputs[nextIndex].ref.txhash == ref.txhash) && (inputs[nextIndex].ref.index == it)
if (nextIndex >= inputs.size || !nextStateIsEncumbrance()) {
throw TransactionVerificationException.TransactionMissingEncumbranceException(
id,
it,
TransactionVerificationException.Direction.INPUT)
}
}
}
}
}

View File

@ -1,19 +1,15 @@
package net.corda.core.transactions
import net.corda.core.contracts.AttachmentResolutionException
import net.corda.core.contracts.NamedByHash
import net.corda.core.contracts.TransactionResolutionException
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.contracts.*
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.getOrThrow
import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializedBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.corda.core.utilities.NonEmptySet
import net.corda.core.utilities.toNonEmptySet
import java.security.PublicKey
import java.security.SignatureException
import java.util.*
@ -32,104 +28,50 @@ import java.util.*
* sign.
*/
// DOCSTART 1
data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
val sigs: List<DigitalSignature.WithKey>
) : NamedByHash {
data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
override val sigs: List<DigitalSignature.WithKey>
) : TransactionWithSignatures {
// DOCEND 1
constructor(wtx: WireTransaction, sigs: List<DigitalSignature.WithKey>) : this(wtx.serialize(), sigs) {
cachedTransaction = wtx
constructor(ctx: CoreTransaction, sigs: List<DigitalSignature.WithKey>) : this(ctx.serialize(), sigs) {
cachedTransaction = ctx
}
init {
require(sigs.isNotEmpty())
require(sigs.isNotEmpty()) { "Tried to instantiate a ${SignedTransaction::class.java.simpleName} without any signatures " }
}
/** Cache the deserialized form of the transaction. This is useful when building a transaction or collecting signatures. */
@Volatile @Transient private var cachedTransaction: WireTransaction? = null
@Volatile @Transient private var cachedTransaction: CoreTransaction? = null
/** Lazily calculated access to the deserialised/hashed transaction data. */
val tx: WireTransaction get() = cachedTransaction ?: txBits.deserialize().apply { cachedTransaction = this }
private val transaction: CoreTransaction get() = cachedTransaction ?: txBits.deserialize().apply { cachedTransaction = this }
/** The id of the contained [WireTransaction]. */
override val id: SecureHash get() = tx.id
override val id: SecureHash get() = transaction.id
@CordaSerializable
class SignaturesMissingException(val missing: NonEmptySet<PublicKey>, val descriptions: List<String>, override val id: SecureHash)
: NamedByHash, SignatureException("Missing signatures for $descriptions on transaction ${id.prefixChars()} for ${missing.joinToString()}")
/** Returns the contained [WireTransaction], or throws if this is a notary change transaction */
val tx: WireTransaction get() = transaction as WireTransaction
/**
* Verifies the signatures on this transaction and throws if any are missing. In this context, "verifying" means
* checking they are valid signatures and that their public keys are in the contained transactions
* [BaseTransaction.mustSign] property.
*
* @throws SignatureException if any signatures are invalid or unrecognised.
* @throws SignaturesMissingException if any signatures should have been present but were not.
*/
@Throws(SignatureException::class)
fun verifyRequiredSignatures() = verifySignaturesExcept()
/** Returns the contained [NotaryChangeWireTransaction], or throws if this is a normal transaction */
val notaryChangeTx: NotaryChangeWireTransaction get() = transaction as NotaryChangeWireTransaction
/**
* Verifies the signatures on this transaction and throws if any are missing which aren't passed as parameters.
* In this context, "verifying" means checking they are valid signatures and that their public keys are in
* the contained transactions [BaseTransaction.mustSign] property.
*
* Normally you would not provide any keys to this function, but if you're in the process of building a partial
* transaction and you want to access the contents before you've signed it, you can specify your own keys here
* to bypass that check.
*
* @throws SignatureException if any signatures are invalid or unrecognised.
* @throws SignaturesMissingException if any signatures should have been present but were not.
*/
// DOCSTART 2
@Throws(SignatureException::class)
fun verifySignaturesExcept(vararg allowedToBeMissing: PublicKey): WireTransaction {
// DOCEND 2
// Embedded WireTransaction is not deserialised until after we check the signatures.
checkSignaturesAreValid()
/** Helper to access the inputs of the contained transaction */
val inputs: List<StateRef> get() = transaction.inputs
/** Helper to access the notary of the contained transaction */
val notary: Party? get() = transaction.notary
val needed = getMissingSignatures() - allowedToBeMissing
if (needed.isNotEmpty())
throw SignaturesMissingException(needed.toNonEmptySet(), getMissingKeyDescriptions(needed), id)
return tx
}
override val requiredSigningKeys: Set<PublicKey> get() = tx.requiredSigningKeys
/**
* Mathematically validates the signatures that are present on this transaction. This does not imply that
* the signatures are by the right keys, or that there are sufficient signatures, just that they aren't
* corrupt. If you use this function directly you'll need to do the other checks yourself. Probably you
* want [verifySignaturesExcept] instead.
*
* @throws SignatureException if a signature fails to verify.
*/
@Throws(SignatureException::class)
fun checkSignaturesAreValid() {
for (sig in sigs) {
sig.verify(id.bytes)
}
}
private fun getMissingSignatures(): Set<PublicKey> {
val sigKeys = sigs.map { it.by }.toSet()
// TODO Problem is that we can get single PublicKey wrapped as CompositeKey in allowedToBeMissing/mustSign
// equals on CompositeKey won't catch this case (do we want to single PublicKey be equal to the same key wrapped in CompositeKey with threshold 1?)
val missing = tx.mustSign.filter { !it.isFulfilledBy(sigKeys) }.toSet()
return missing
}
/**
* Get a human readable description of where signatures are required from, and are missing, to assist in debugging
* the underlying cause.
*/
private fun getMissingKeyDescriptions(missing: Set<PublicKey>): ArrayList<String> {
override fun getKeyDescriptions(keys: Set<PublicKey>): ArrayList<String> {
// TODO: We need a much better way of structuring this data
val missingElements = ArrayList<String>()
val descriptions = ArrayList<String>()
this.tx.commands.forEach { command ->
if (command.signers.any { it in missing })
missingElements.add(command.toString())
if (command.signers.any { it in keys })
descriptions.add(command.toString())
}
if (this.tx.notary?.owningKey in missing)
missingElements.add("notary")
return missingElements
if (this.tx.notary?.owningKey in keys)
descriptions.add("notary")
return descriptions
}
/** Returns the same transaction but with an additional (unchecked) signature. */
@ -179,10 +121,9 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
}
/**
* Checks the transaction's signatures are valid, optionally calls [verifyRequiredSignatures]
* to check all required signatures are present, calls [WireTransaction.toLedgerTransaction]
* with the passed in [ServiceHub] to resolve the dependencies and return an unverified
* LedgerTransaction, then verifies the LedgerTransaction.
* Checks the transaction's signatures are valid, optionally calls [verifyRequiredSignatures] to check
* all required signatures are present. Resolves inputs and attachments from the local storage and performs full
* transaction verification, including running the contracts.
*
* @throws AttachmentResolutionException if a required attachment was not found in storage.
* @throws TransactionResolutionException if an input points to a transaction not found in storage.
@ -192,10 +133,41 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
@JvmOverloads
@Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class)
fun verify(services: ServiceHub, checkSufficientSignatures: Boolean = true) {
if (isNotaryChangeTransaction()) {
verifyNotaryChangeTransaction(checkSufficientSignatures, services)
} else {
verifyRegularTransaction(checkSufficientSignatures, services)
}
}
private fun verifyRegularTransaction(checkSufficientSignatures: Boolean, services: ServiceHub) {
checkSignaturesAreValid()
if (checkSufficientSignatures) verifyRequiredSignatures()
tx.toLedgerTransaction(services).verify()
val ltx = tx.toLedgerTransaction(services)
// TODO: allow non-blocking verification
services.transactionVerifierService.verify(ltx).getOrThrow()
}
private fun verifyNotaryChangeTransaction(checkSufficientSignatures: Boolean, services: ServiceHub) {
val ntx = resolveNotaryChangeTransaction(services)
if (checkSufficientSignatures) ntx.verifyRequiredSignatures()
}
fun isNotaryChangeTransaction() = transaction is NotaryChangeWireTransaction
/**
* If [transaction] is a [NotaryChangeWireTransaction], loads the input states and resolves it to a
* [NotaryChangeLedgerTransaction] so the signatures can be verified.
*/
fun resolveNotaryChangeTransaction(services: ServiceHub): NotaryChangeLedgerTransaction {
val ntx = transaction as? NotaryChangeWireTransaction
?: throw IllegalStateException("Expected a ${NotaryChangeWireTransaction::class.simpleName} but found ${transaction::class.simpleName}")
return ntx.resolve(services, sigs)
}
override fun toString(): String = "${javaClass.simpleName}(id=$id)"
@CordaSerializable
class SignaturesMissingException(val missing: Set<PublicKey>, val descriptions: List<String>, override val id: SecureHash)
: NamedByHash, SignatureException("Missing signatures for $descriptions on transaction ${id.prefixChars()} for ${missing.joinToString()}")
}

View File

@ -6,7 +6,6 @@ import net.corda.core.crypto.*
import net.corda.core.identity.Party
import net.corda.core.internal.FlowStateMachine
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.serialize
import java.security.KeyPair
import java.security.PublicKey
import java.security.SignatureException
@ -25,10 +24,6 @@ import java.util.*
* @param notary Notary used for the transaction. If null, this indicates the transaction DOES NOT have a notary.
* When this is set to a non-null value, an output state can be added by just passing in a [ContractState] a
* [TransactionState] with this notary specified will be generated automatically.
*
* @param signers The set of public keys the transaction needs signatures for. The logic for building the signers set
* can be customised for every [TransactionType]. E.g. in the general case it contains the command and notary public keys,
* but for the [TransactionType.NotaryChange] transactions it is the set of all input [ContractState.participants].
*/
open class TransactionBuilder(
protected val type: TransactionType = TransactionType.General,
@ -38,7 +33,6 @@ open class TransactionBuilder(
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 var window: TimeWindow? = null) {
constructor(type: TransactionType, notary: Party) : this(type, notary, (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID())
@ -52,7 +46,6 @@ open class TransactionBuilder(
attachments = ArrayList(attachments),
outputs = ArrayList(outputs),
commands = ArrayList(commands),
signers = LinkedHashSet(signers),
window = window
)
@ -76,7 +69,7 @@ open class TransactionBuilder(
// DOCEND 1
fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments),
ArrayList(outputs), ArrayList(commands), notary, signers.toList(), type, window)
ArrayList(outputs), ArrayList(commands), notary, type, window)
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
fun toLedgerTransaction(services: ServiceHub) = toWireTransaction().toLedgerTransaction(services)
@ -89,7 +82,6 @@ open class TransactionBuilder(
open fun addInputState(stateAndRef: StateAndRef<*>): TransactionBuilder {
val notary = stateAndRef.state.notary
require(notary == this.notary) { "Input state requires notary \"$notary\" which does not match the transaction notary \"${this.notary}\"." }
signers.add(notary.owningKey)
inputs.add(stateAndRef.ref)
return this
}
@ -117,8 +109,6 @@ open class TransactionBuilder(
}
fun addCommand(arg: Command<*>): TransactionBuilder {
// TODO: replace pubkeys in commands with 'pointers' to keys in signers
signers.addAll(arg.signers)
commands.add(arg)
return this
}
@ -133,7 +123,6 @@ open class TransactionBuilder(
*/
fun setTimeWindow(timeWindow: TimeWindow): TransactionBuilder {
check(notary != null) { "Only notarised transactions can have a time-window" }
signers.add(notary!!.owningKey)
window = timeWindow
return this
}
@ -175,7 +164,8 @@ open class TransactionBuilder(
fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedTransaction {
if (checkSufficientSignatures) {
val gotKeys = currentSigs.map { it.by }.toSet()
val missing: Set<PublicKey> = signers.filter { !it.isFulfilledBy(gotKeys) }.toSet()
val requiredKeys = commands.flatMap { it.signers }.toSet()
val missing: Set<PublicKey> = requiredKeys.filter { !it.isFulfilledBy(gotKeys) }.toSet()
if (missing.isNotEmpty())
throw IllegalStateException("Missing signatures on the transaction for the public keys: ${missing.joinToString()}")
}

View File

@ -0,0 +1,79 @@
package net.corda.core.transactions
import net.corda.core.contracts.NamedByHash
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.transactions.SignedTransaction.SignaturesMissingException
import net.corda.core.utilities.toNonEmptySet
import java.security.PublicKey
import java.security.SignatureException
/** An interface for transactions containing signatures, with logic for signature verification */
interface TransactionWithSignatures : NamedByHash {
val sigs: List<DigitalSignature.WithKey>
/** Specifies all the public keys that require signatures for the transaction to be valid */
val requiredSigningKeys: Set<PublicKey>
/**
* Verifies the signatures on this transaction and throws if any are missing. In this context, "verifying" means
* checking they are valid signatures and that their public keys are in the [requiredSigningKeys] set.
*
* @throws SignatureException if any signatures are invalid or unrecognised.
* @throws SignaturesMissingException if any signatures should have been present but were not.
*/
@Throws(SignatureException::class)
fun verifyRequiredSignatures() = verifySignaturesExcept()
/**
* Verifies the signatures on this transaction and throws if any are missing which aren't passed as parameters.
* In this context, "verifying" means checking they are valid signatures and that their public keys are in
* the [requiredSigningKeys] set.
*
* Normally you would not provide any keys to this function, but if you're in the process of building a partial
* transaction and you want to access the contents before you've signed it, you can specify your own keys here
* to bypass that check.
*
* @throws SignatureException if any signatures are invalid or unrecognised.
* @throws SignaturesMissingException if any signatures should have been present but were not.
*/
@Throws(SignatureException::class)
fun verifySignaturesExcept(vararg allowedToBeMissing: PublicKey) {
checkSignaturesAreValid()
val needed = getMissingSignatures() - allowedToBeMissing
if (needed.isNotEmpty())
throw SignaturesMissingException(needed.toNonEmptySet(), getKeyDescriptions(needed), id)
}
/**
* Mathematically validates the signatures that are present on this transaction. This does not imply that
* the signatures are by the right keys, or that there are sufficient signatures, just that they aren't
* corrupt. If you use this function directly you'll need to do the other checks yourself. Probably you
* want [verifySignatures] instead.
*
* @throws SignatureException if a signature fails to verify.
*/
@Throws(SignatureException::class)
fun checkSignaturesAreValid() {
for (sig in sigs) {
sig.verify(id.bytes)
}
}
/**
* Get a human readable description of where signatures are required from, and are missing, to assist in debugging
* the underlying cause.
*
* Note that the results should not be serialised, parsed or expected to remain stable between Corda versions.
*/
fun getKeyDescriptions(keys: Set<PublicKey>): List<String>
private fun getMissingSignatures(): Set<PublicKey> {
val sigKeys = sigs.map { it.by }.toSet()
// TODO Problem is that we can get single PublicKey wrapped as CompositeKey in allowedToBeMissing/mustSign
// equals on CompositeKey won't catch this case (do we want to single PublicKey be equal to the same key wrapped in CompositeKey with threshold 1?)
val missing = requiredSigningKeys.filter { !it.isFulfilledBy(sigKeys) }.toSet()
return missing
}
}

View File

@ -17,25 +17,41 @@ import java.util.function.Predicate
* by a [SignedTransaction] that carries the signatures over this payload.
* The identity of the transaction is the Merkle tree root of its components (see [MerkleTree]).
*/
class WireTransaction(
data class WireTransaction(
/** Pointers to the input states on the ledger, identified by (tx identity hash, output index). */
override val inputs: List<StateRef>,
/** Hashes of the ZIP/JAR files that are needed to interpret the contents of this wire transaction. */
override val attachments: List<SecureHash>,
outputs: List<TransactionState<ContractState>>,
override val outputs: List<TransactionState<ContractState>>,
/** Ordered list of ([CommandData], [PublicKey]) pairs that instruct the contracts what to do. */
override val commands: List<Command<*>>,
notary: Party?,
signers: List<PublicKey>,
type: TransactionType,
timeWindow: TimeWindow?
) : BaseTransaction(inputs, outputs, notary, signers, type, timeWindow), TraversableTransaction {
override val notary: Party?,
// TODO: remove type
override val type: TransactionType,
override val timeWindow: TimeWindow?
) : CoreTransaction(), TraversableTransaction {
init {
checkInvariants()
checkBaseInvariants()
if (timeWindow != null) check(notary != null) { "Transactions with time-windows must be notarised" }
check(availableComponents.isNotEmpty()) { "A WireTransaction cannot be empty" }
}
/** The transaction id is represented by the root hash of Merkle tree over the transaction components. */
override val id: SecureHash by lazy { merkleTree.hash }
override val id: SecureHash get() = merkleTree.hash
override val availableComponents: List<Any>
get() = listOf(inputs, attachments, outputs, commands).flatten() + listOf(notary, type, timeWindow).filterNotNull()
/** Public keys that need to be fulfilled by signatures in order for the transaction to be valid. */
val requiredSigningKeys: Set<PublicKey> get() {
val commandKeys = commands.flatMap { it.signers }.toSet()
// TODO: prevent notary field from being set if there are no inputs and no timestamp
return if (notary != null && (inputs.isNotEmpty() || timeWindow != null)) {
commandKeys + notary.owningKey
} else {
commandKeys
}
}
/**
* Looks up identities and attachments from storage to generate a [LedgerTransaction]. A transaction is expected to
@ -76,7 +92,7 @@ class WireTransaction(
val resolvedInputs = inputs.map { ref ->
resolveStateRef(ref)?.let { StateAndRef(it, ref) } ?: throw TransactionResolutionException(ref.txhash)
}
return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, mustSign, timeWindow, type)
return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, timeWindow, type)
}
/**
@ -104,7 +120,6 @@ class WireTransaction(
outputs.filter { filtering.test(it) },
commands.filter { filtering.test(it) },
notNullFalse(notary) as Party?,
mustSign.filter { filtering.test(it) },
notNullFalse(type) as TransactionType?,
notNullFalse(timeWindow) as TimeWindow?
)
@ -130,30 +145,4 @@ class WireTransaction(
for (attachment in attachments) buf.appendln("${Emoji.paperclip}ATTACHMENT: $attachment")
return buf.toString()
}
// TODO: When Kotlin 1.1 comes out we can make this class a data class again, and have these be autogenerated.
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
if (!super.equals(other)) return false
other as WireTransaction
if (inputs != other.inputs) return false
if (attachments != other.attachments) return false
if (outputs != other.outputs) return false
if (commands != other.commands) return false
return true
}
override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + inputs.hashCode()
result = 31 * result + attachments.hashCode()
result = 31 * result + outputs.hashCode()
result = 31 * result + commands.hashCode()
return result
}
}

View File

@ -6,7 +6,6 @@ import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.crypto.generateKeyPair
import net.corda.core.crypto.sign
import net.corda.core.identity.Party
import net.corda.core.serialization.SerializedBytes
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
@ -18,8 +17,10 @@ import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class TransactionTests : TestDependencyInjectionBase() {
private fun makeSigned(wtx: WireTransaction, vararg keys: KeyPair): SignedTransaction {
return SignedTransaction(wtx, keys.map { it.sign(wtx.id.bytes) })
private fun makeSigned(wtx: WireTransaction, vararg keys: KeyPair, notarySig: Boolean = true): SignedTransaction {
val keySigs = keys.map { it.sign(wtx.id.bytes) }
val sigs = if (notarySig) keySigs + DUMMY_NOTARY_KEY.sign(wtx.id.bytes) else keySigs
return SignedTransaction(wtx, sigs)
}
@Test
@ -36,9 +37,8 @@ class TransactionTests : TestDependencyInjectionBase() {
inputs = listOf(StateRef(SecureHash.randomSHA256(), 0)),
attachments = emptyList(),
outputs = emptyList(),
commands = emptyList(),
commands = listOf(dummyCommand(compKey, DUMMY_KEY_1.public, DUMMY_KEY_2.public)),
notary = DUMMY_NOTARY,
signers = listOf(compKey, DUMMY_KEY_1.public, DUMMY_KEY_2.public),
type = TransactionType.General,
timeWindow = null
)
@ -64,13 +64,12 @@ class TransactionTests : TestDependencyInjectionBase() {
inputs = listOf(StateRef(SecureHash.randomSHA256(), 0)),
attachments = emptyList(),
outputs = emptyList(),
commands = emptyList(),
commands = listOf(dummyCommand(DUMMY_KEY_1.public, DUMMY_KEY_2.public)),
notary = DUMMY_NOTARY,
signers = listOf(DUMMY_KEY_1.public, DUMMY_KEY_2.public),
type = TransactionType.General,
timeWindow = null
)
assertFailsWith<IllegalArgumentException> { makeSigned(wtx).verifyRequiredSignatures() }
assertFailsWith<IllegalArgumentException> { makeSigned(wtx, notarySig = false).verifyRequiredSignatures() }
assertEquals(
setOf(DUMMY_KEY_1.public),
@ -99,7 +98,6 @@ class TransactionTests : TestDependencyInjectionBase() {
val commands = emptyList<AuthenticatedObject<CommandData>>()
val attachments = emptyList<Attachment>()
val id = SecureHash.randomSHA256()
val signers = listOf(DUMMY_NOTARY_KEY.public)
val timeWindow: TimeWindow? = null
val transaction: LedgerTransaction = LedgerTransaction(
inputs,
@ -108,39 +106,27 @@ class TransactionTests : TestDependencyInjectionBase() {
attachments,
id,
null,
signers,
timeWindow,
TransactionType.General
)
transaction.type.verify(transaction)
transaction.verify()
}
@Test
fun `transaction verification fails for duplicate inputs`() {
val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE), DUMMY_NOTARY)
fun `transaction cannot have duplicate inputs`() {
val stateRef = StateRef(SecureHash.randomSHA256(), 0)
val stateAndRef = StateAndRef(baseOutState, stateRef)
val inputs = listOf(stateAndRef, stateAndRef)
val outputs = listOf(baseOutState)
val commands = emptyList<AuthenticatedObject<CommandData>>()
val attachments = emptyList<Attachment>()
val id = SecureHash.randomSHA256()
val signers = listOf(DUMMY_NOTARY_KEY.public)
val timeWindow: TimeWindow? = null
val transaction: LedgerTransaction = LedgerTransaction(
inputs,
outputs,
commands,
attachments,
id,
DUMMY_NOTARY,
signers,
timeWindow,
TransactionType.General
fun buildTransaction() = WireTransaction(
inputs = listOf(stateRef, stateRef),
attachments = emptyList(),
outputs = emptyList(),
commands = listOf(dummyCommand(DUMMY_KEY_1.public, DUMMY_KEY_2.public)),
notary = DUMMY_NOTARY,
type = TransactionType.General,
timeWindow = null
)
assertFailsWith<TransactionVerificationException.DuplicateInputStates> { transaction.type.verify(transaction) }
assertFailsWith<IllegalStateException> { buildTransaction() }
}
@Test
@ -153,20 +139,18 @@ class TransactionTests : TestDependencyInjectionBase() {
val commands = emptyList<AuthenticatedObject<CommandData>>()
val attachments = emptyList<Attachment>()
val id = SecureHash.randomSHA256()
val signers = listOf(DUMMY_NOTARY_KEY.public)
val timeWindow: TimeWindow? = null
val transaction: LedgerTransaction = LedgerTransaction(
fun buildTransaction() = LedgerTransaction(
inputs,
outputs,
commands,
attachments,
id,
notary,
signers,
timeWindow,
TransactionType.General
)
assertFailsWith<TransactionVerificationException.NotaryChangeInWrongTransactionType> { transaction.type.verify(transaction) }
assertFailsWith<TransactionVerificationException.NotaryChangeInWrongTransactionType> { buildTransaction() }
}
}

View File

@ -16,7 +16,7 @@ class AllOfTests {
fun minimal() {
val counter = AtomicInteger(0)
val clause = AllOf(matchedClause(counter), matchedClause(counter))
val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, emptyList(), null, TransactionType.General)
val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, null, TransactionType.General)
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
// Check that we've run the verify() function of two clauses
@ -26,7 +26,7 @@ class AllOfTests {
@Test
fun `not all match`() {
val clause = AllOf(matchedClause(), unmatchedClause())
val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, emptyList(), null, TransactionType.General)
val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, null, TransactionType.General)
assertFailsWith<IllegalStateException> { verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>()) }
}
}

View File

@ -15,7 +15,7 @@ class AnyOfTests {
fun minimal() {
val counter = AtomicInteger(0)
val clause = AnyOf(matchedClause(counter), matchedClause(counter))
val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, emptyList(), null, TransactionType.General)
val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, null, TransactionType.General)
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
// Check that we've run the verify() function of two clauses
@ -26,7 +26,7 @@ class AnyOfTests {
fun `not all match`() {
val counter = AtomicInteger(0)
val clause = AnyOf(matchedClause(counter), unmatchedClause(counter))
val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, emptyList(), null, TransactionType.General)
val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, null, TransactionType.General)
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
// Check that we've run the verify() function of one clause
@ -37,7 +37,7 @@ class AnyOfTests {
fun `none match`() {
val counter = AtomicInteger(0)
val clause = AnyOf(unmatchedClause(counter), unmatchedClause(counter))
val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, emptyList(), null, TransactionType.General)
val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, null, TransactionType.General)
assertFailsWith(IllegalArgumentException::class) {
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
}

View File

@ -23,7 +23,7 @@ class VerifyClausesTests {
outputs: List<ContractState>,
commands: List<AuthenticatedObject<CommandData>>, groupingKey: Unit?): Set<CommandData> = emptySet()
}
val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, emptyList(), null, TransactionType.General)
val tx = LedgerTransaction(emptyList(), emptyList(), emptyList(), emptyList(), SecureHash.randomSHA256(), null, null, TransactionType.General)
verifyClause(tx, clause, emptyList<AuthenticatedObject<CommandData>>())
}
@ -36,7 +36,7 @@ class VerifyClausesTests {
commands: List<AuthenticatedObject<CommandData>>, groupingKey: Unit?): Set<CommandData> = emptySet()
}
val command = AuthenticatedObject(emptyList(), emptyList(), DummyContract.Commands.Create())
val tx = LedgerTransaction(emptyList(), emptyList(), listOf(command), emptyList(), SecureHash.randomSHA256(), null, emptyList(), null, TransactionType.General)
val tx = LedgerTransaction(emptyList(), emptyList(), listOf(command), emptyList(), SecureHash.randomSHA256(), null, null, TransactionType.General)
// The clause is matched, but doesn't mark the command as consumed, so this should error
assertFailsWith<IllegalStateException> { verifyClause(tx, clause, listOf(command)) }
}

View File

@ -116,7 +116,6 @@ class PartialMerkleTreeTest : TestDependencyInjectionBase() {
assertEquals(1, leaves.commands.size)
assertEquals(1, leaves.outputs.size)
assertEquals(1, leaves.inputs.size)
assertEquals(1, leaves.mustSign.size)
assertEquals(0, leaves.attachments.size)
assertTrue(mt.filteredLeaves.timeWindow != null)
assertEquals(null, mt.filteredLeaves.type)
@ -229,7 +228,6 @@ class PartialMerkleTreeTest : TestDependencyInjectionBase() {
outputs = testTx.outputs,
commands = testTx.commands,
notary = notary,
signers = listOf(MEGA_CORP_PUBKEY, DUMMY_PUBKEY_1),
type = TransactionType.General,
timeWindow = timeWindow
)

View File

@ -46,15 +46,13 @@ class MyValidatingNotaryFlow(otherSide: Party, service: MyCustomValidatingNotary
override fun receiveAndVerifyTx(): TransactionParts {
val stx = receive<SignedTransaction>(otherSide).unwrap { it }
checkSignatures(stx)
resolveTransaction(stx)
validateTransaction(stx)
val wtx = stx.tx
validateTransaction(wtx)
val ltx = validateTransaction(wtx)
processTransaction(ltx)
return TransactionParts(wtx.id, wtx.inputs, wtx.timeWindow)
}
fun processTransaction(ltx: LedgerTransaction) {
fun processTransaction(stx: SignedTransaction) {
// Add custom transaction processing logic here
}
@ -67,12 +65,10 @@ class MyValidatingNotaryFlow(otherSide: Party, service: MyCustomValidatingNotary
}
@Suspendable
fun validateTransaction(wtx: WireTransaction): LedgerTransaction {
fun validateTransaction(stx: SignedTransaction) {
try {
resolveTransaction(wtx)
val ltx = wtx.toLedgerTransaction(serviceHub)
ltx.verify()
return ltx
resolveTransaction(stx)
stx.verify(serviceHub, false)
} catch (e: Exception) {
throw when (e) {
is TransactionVerificationException,
@ -83,6 +79,6 @@ class MyValidatingNotaryFlow(otherSide: Party, service: MyCustomValidatingNotary
}
@Suspendable
private fun resolveTransaction(wtx: WireTransaction) = subFlow(ResolveTransactionsFlow(wtx, otherSide))
private fun resolveTransaction(stx: SignedTransaction) = subFlow(ResolveTransactionsFlow(stx, otherSide))
}
// END 2

View File

@ -227,16 +227,17 @@ class RecordCompletionFlow(val source: Party) : FlowLogic<Unit>() {
// First we receive the verdict transaction signed by their single key
val completeTx = receive<SignedTransaction>(source).unwrap {
// Check the transaction is signed apart from our own key and the notary
val wtx = it.verifySignaturesExcept(serviceHub.myInfo.legalIdentity.owningKey, it.tx.notary!!.owningKey)
it.verifySignaturesExcept(serviceHub.myInfo.legalIdentity.owningKey, it.tx.notary!!.owningKey)
// Check the transaction data is correctly formed
wtx.toLedgerTransaction(serviceHub).verify()
val ltx = it.toLedgerTransaction(serviceHub, false)
ltx.verify()
// Confirm that this is the expected type of transaction
require(wtx.commands.single().value is TradeApprovalContract.Commands.Completed) {
require(ltx.commands.single().value is TradeApprovalContract.Commands.Completed) {
"Transaction must represent a workflow completion"
}
// Check the context dependent parts of the transaction as the
// Contract verify method must not use serviceHub queries.
val state = wtx.outRef<TradeApprovalContract.State>(0)
val state = ltx.outRef<TradeApprovalContract.State>(0)
require(state.state.data.source == serviceHub.myInfo.legalIdentity) {
"Proposal not one of our original proposals"
}

View File

@ -73,7 +73,6 @@ class WiredTransactionGenerator : Generator<WireTransaction>(WireTransaction::cl
outputs = TransactionStateGenerator(ContractStateGenerator()).generateList(random, status),
commands = commands,
notary = PartyGenerator().generate(random, status),
signers = commands.flatMap { it.signers },
type = TransactionType.General,
timeWindow = TimeWindowGenerator().generate(random, status)
)
@ -84,7 +83,7 @@ class SignedTransactionGenerator : Generator<SignedTransaction>(SignedTransactio
override fun generate(random: SourceOfRandomness, status: GenerationStatus): SignedTransaction {
val wireTransaction = WiredTransactionGenerator().generate(random, status)
return SignedTransaction(
wtx = wireTransaction,
ctx = wireTransaction,
sigs = listOf(NullSignature)
)
}

View File

@ -116,7 +116,6 @@ class VaultSchemaTest : TestDependencyInjectionBase() {
val commands = emptyList<AuthenticatedObject<CommandData>>()
val attachments = emptyList<Attachment>()
val id = SecureHash.randomSHA256()
val signers = listOf(DUMMY_NOTARY_KEY.public)
val timeWindow: TimeWindow? = null
transaction = LedgerTransaction(
inputs,
@ -125,7 +124,6 @@ class VaultSchemaTest : TestDependencyInjectionBase() {
attachments,
id,
notary,
signers,
timeWindow,
TransactionType.General
)
@ -148,7 +146,6 @@ class VaultSchemaTest : TestDependencyInjectionBase() {
val commands = emptyList<AuthenticatedObject<CommandData>>()
val attachments = emptyList<Attachment>()
val id = SecureHash.randomSHA256()
val signers = listOf(DUMMY_NOTARY_KEY.public)
val timeWindow: TimeWindow? = null
return LedgerTransaction(
inputs,
@ -157,7 +154,6 @@ class VaultSchemaTest : TestDependencyInjectionBase() {
attachments,
id,
notary,
signers,
timeWindow,
TransactionType.General
)

View File

@ -1,7 +1,10 @@
package net.corda.node.services
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.*
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.UpgradeCommand
import net.corda.core.contracts.UpgradedContract
import net.corda.core.contracts.requireThat
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.*
import net.corda.core.identity.AnonymousPartyAndPath
@ -63,6 +66,7 @@ class NotifyTransactionHandler(val otherParty: Party) : FlowLogic<Unit>() {
override fun call() {
val request = receive<BroadcastTransactionFlow.NotifyTxRequest>(otherParty).unwrap { it }
subFlow(ResolveTransactionsFlow(request.tx, otherParty))
request.tx.verify(serviceHub)
serviceHub.recordTransactions(request.tx)
}
}
@ -77,26 +81,18 @@ class NotaryChangeHandler(otherSide: Party) : AbstractStateReplacementFlow.Accep
*/
override fun verifyProposal(proposal: AbstractStateReplacementFlow.Proposal<Party>): Unit {
val state = proposal.stateRef
val proposedTx = proposal.stx.tx
val proposedTx = proposal.stx.resolveNotaryChangeTransaction(serviceHub)
val newNotary = proposal.modification
if (proposedTx.type !is TransactionType.NotaryChange) {
throw StateReplacementException("The proposed transaction is not a notary change transaction.")
if (state !in proposedTx.inputs.map { it.ref }) {
throw StateReplacementException("The proposed state $state is not in the proposed transaction inputs")
}
val newNotary = proposal.modification
// TODO: load and compare against notary whitelist from config. Remove the check below
val isNotary = serviceHub.networkMapCache.notaryNodes.any { it.notaryIdentity == newNotary }
if (!isNotary) {
throw StateReplacementException("The proposed node $newNotary does not run a Notary service")
}
if (state !in proposedTx.inputs) {
throw StateReplacementException("The proposed state $state is not in the proposed transaction inputs")
}
// // An example requirement
// val blacklist = listOf("Evil Notary")
// checkProposal(newNotary.name !in blacklist) {
// "The proposed new notary $newNotary is not trusted by the party"
// }
}
}

View File

@ -123,7 +123,9 @@ interface ServiceHubInternal : PluginServiceHub {
} else {
log.warn("Transactions recorded from outside of a state machine")
}
vaultService.notifyAll(recordedTransactions.map { it.tx })
val toNotify = txs.map { if (it.isNotaryChangeTransaction()) it.notaryChangeTx else it.tx }
vaultService.notifyAll(toNotify)
}
/**

View File

@ -6,6 +6,7 @@ import net.corda.core.flows.TransactionParts
import net.corda.core.identity.Party
import net.corda.core.node.services.TrustedAuthorityNotaryService
import net.corda.core.transactions.FilteredTransaction
import net.corda.core.transactions.NotaryChangeWireTransaction
import net.corda.core.utilities.unwrap
class NonValidatingNotaryFlow(otherSide: Party, service: TrustedAuthorityNotaryService) : NotaryFlow.Service(otherSide, service) {
@ -19,10 +20,19 @@ class NonValidatingNotaryFlow(otherSide: Party, service: TrustedAuthorityNotaryS
*/
@Suspendable
override fun receiveAndVerifyTx(): TransactionParts {
val ftx = receive<FilteredTransaction>(otherSide).unwrap {
val parts = receive<Any>(otherSide).unwrap {
when (it) {
is FilteredTransaction -> {
it.verify()
it
TransactionParts(it.rootHash, it.filteredLeaves.inputs, it.filteredLeaves.timeWindow)
}
return TransactionParts(ftx.rootHash, ftx.filteredLeaves.inputs, ftx.filteredLeaves.timeWindow)
is NotaryChangeWireTransaction -> TransactionParts(it.id, it.inputs, null)
else -> {
throw IllegalArgumentException("Received unexpected transaction type: ${it::class.java.simpleName}," +
"expected either ${FilteredTransaction::class.java.simpleName} or ${NotaryChangeWireTransaction::class.java.simpleName}")
}
}
}
return parts
}
}

View File

@ -26,8 +26,8 @@ class ValidatingNotaryFlow(otherSide: Party, service: TrustedAuthorityNotaryServ
override fun receiveAndVerifyTx(): TransactionParts {
val stx = receive<SignedTransaction>(otherSide).unwrap { it }
checkSignatures(stx)
validateTransaction(stx)
val wtx = stx.tx
validateTransaction(wtx)
return TransactionParts(wtx.id, wtx.inputs, wtx.timeWindow)
}
@ -40,10 +40,10 @@ class ValidatingNotaryFlow(otherSide: Party, service: TrustedAuthorityNotaryServ
}
@Suspendable
fun validateTransaction(wtx: WireTransaction) {
fun validateTransaction(stx: SignedTransaction) {
try {
resolveTransaction(wtx)
wtx.toLedgerTransaction(serviceHub).verify()
resolveTransaction(stx)
stx.verify(serviceHub, false)
} catch (e: Exception) {
throw when (e) {
is TransactionVerificationException,
@ -54,5 +54,5 @@ class ValidatingNotaryFlow(otherSide: Party, service: TrustedAuthorityNotaryServ
}
@Suspendable
private fun resolveTransaction(wtx: WireTransaction) = subFlow(ResolveTransactionsFlow(wtx, otherSide))
private fun resolveTransaction(stx: SignedTransaction) = subFlow(ResolveTransactionsFlow(stx, otherSide))
}

View File

@ -12,14 +12,15 @@ 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.internal.ThreadBox
import net.corda.core.internal.bufferUntilSubscribed
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.bufferUntilSubscribed
import net.corda.core.internal.tee
import net.corda.core.messaging.DataFeed
import net.corda.core.node.ServiceHub
import net.corda.core.node.services.StatesNotAvailableException
@ -30,7 +31,8 @@ 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.internal.tee
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.*
@ -184,15 +186,118 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
return stateAndRefs.associateBy({ it.ref }, { it.state })
}
override fun notifyAll(txns: Iterable<WireTransaction>) {
/**
* Splits the provided [txns] into batches of [WireTransaction] and [NotaryChangeWireTransaction].
* This is required because the batches get aggregated into single updates, and we want to be able to
* indicate whether an update consists entirely of regular or notary change transactions, which may require
* different processing logic.
*/
override fun notifyAll(txns: Iterable<CoreTransaction>) {
// It'd be easier to just group by type, but then we'd lose ordering.
val regularTxns = mutableListOf<WireTransaction>()
val notaryChangeTxns = mutableListOf<NotaryChangeWireTransaction>()
for (tx in txns) {
when (tx) {
is WireTransaction -> {
regularTxns.add(tx)
if (notaryChangeTxns.isNotEmpty()) {
notifyNotaryChange(notaryChangeTxns.toList())
notaryChangeTxns.clear()
}
}
is NotaryChangeWireTransaction -> {
notaryChangeTxns.add(tx)
if (regularTxns.isNotEmpty()) {
notifyRegular(regularTxns.toList())
regularTxns.clear()
}
}
}
}
if (regularTxns.isNotEmpty()) notifyRegular(regularTxns.toList())
if (notaryChangeTxns.isNotEmpty()) notifyNotaryChange(notaryChangeTxns.toList())
}
private fun notifyRegular(txns: Iterable<WireTransaction>) {
val ourKeys = services.keyManagementService.keys
val netDelta = txns.fold(Vault.NoUpdate) { netDelta, txn -> netDelta + makeUpdate(txn, ourKeys) }
if (netDelta != Vault.NoUpdate) {
recordUpdate(netDelta)
fun makeUpdate(tx: WireTransaction): Vault.Update<ContractState> {
val ourNewStates = tx.outputs.
filter { isRelevant(it.data, ourKeys) }.
map { tx.outRef<ContractState>(it.data) }
// Retrieve all unconsumed states for this transaction's inputs
val consumedStates = loadStates(tx.inputs)
// Is transaction irrelevant?
if (consumedStates.isEmpty() && ourNewStates.isEmpty()) {
log.trace { "tx ${tx.id} was irrelevant to this vault, ignoring" }
return Vault.NoUpdate
}
return Vault.Update(consumedStates, ourNewStates.toHashSet())
}
val netDelta = txns.fold(Vault.NoUpdate) { netDelta, txn -> netDelta + makeUpdate(txn) }
processAndNotify(netDelta)
}
private fun notifyNotaryChange(txns: Iterable<NotaryChangeWireTransaction>) {
val ourKeys = services.keyManagementService.keys
fun makeUpdate(tx: NotaryChangeWireTransaction): Vault.Update<ContractState> {
// We need to resolve the full transaction here because outputs are calculated from inputs
// We also can't do filtering beforehand, since output encumbrance pointers get recalculated based on
// input position
val ltx = tx.resolve(services, emptyList())
val (consumedStateAndRefs, producedStates) = ltx.inputs.
zip(ltx.outputs).
filter {
(_, output) ->
isRelevant(output.data, ourKeys)
}.
unzip()
val producedStateAndRefs = producedStates.map { ltx.outRef<ContractState>(it.data) }
if (consumedStateAndRefs.isEmpty() && producedStateAndRefs.isEmpty()) {
log.trace { "tx ${tx.id} was irrelevant to this vault, ignoring" }
return Vault.NoUpdate
}
return Vault.Update(consumedStateAndRefs.toHashSet(), producedStateAndRefs.toHashSet())
}
val netDelta = txns.fold(Vault.NoUpdate) { netDelta, txn -> netDelta + makeUpdate(txn) }
processAndNotify(netDelta)
}
private fun loadStates(refs: Collection<StateRef>): HashSet<StateAndRef<ContractState>> {
val states = HashSet<StateAndRef<ContractState>>()
if (refs.isNotEmpty()) {
session.withTransaction(TransactionIsolation.REPEATABLE_READ) {
val result = select(VaultStatesEntity::class).
where(stateRefCompositeColumn.`in`(stateRefArgs(refs))).
and(VaultSchema.VaultStates::stateStatus eq Vault.StateStatus.UNCONSUMED)
result.get().forEach {
val txHash = SecureHash.parse(it.txId)
val index = it.index
val state = it.contractState.deserialize<TransactionState<ContractState>>(context = STORAGE_CONTEXT)
states.add(StateAndRef(state, StateRef(txHash, index)))
}
}
}
return states
}
private fun processAndNotify(update: Vault.Update<ContractState>) {
if (update != Vault.NoUpdate) {
recordUpdate(update)
mutex.locked {
// flowId required by SoftLockManager to perform auto-registration of soft locks for new states
val uuid = (Strand.currentStrand() as? FlowStateMachineImpl<*>)?.id?.uuid
val vaultUpdate = if (uuid != null) netDelta.copy(flowId = uuid) else netDelta
val vaultUpdate = if (uuid != null) update.copy(flowId = uuid) else update
updatesPublisher.onNext(vaultUpdate)
}
}
@ -420,35 +525,6 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P
private fun deriveState(txState: TransactionState<Cash.State>, amount: Amount<Issued<Currency>>, owner: AbstractParty)
= txState.copy(data = txState.data.copy(amount = amount, owner = owner))
@VisibleForTesting
internal fun makeUpdate(tx: WireTransaction, ourKeys: Set<PublicKey>): Vault.Update<ContractState> {
val ourNewStates = tx.filterOutRefs<ContractState> { isRelevant(it, ourKeys) }
// Retrieve all unconsumed states for this transaction's inputs
val consumedStates = HashSet<StateAndRef<ContractState>>()
if (tx.inputs.isNotEmpty()) {
session.withTransaction(TransactionIsolation.REPEATABLE_READ) {
val result = select(VaultStatesEntity::class).
where(stateRefCompositeColumn.`in`(stateRefArgs(tx.inputs))).
and(VaultSchema.VaultStates::stateStatus eq Vault.StateStatus.UNCONSUMED)
result.get().forEach {
val txHash = SecureHash.parse(it.txId)
val index = it.index
val state = it.contractState.deserialize<TransactionState<ContractState>>(context = STORAGE_CONTEXT)
consumedStates.add(StateAndRef(state, StateRef(txHash, index)))
}
}
}
// Is transaction irrelevant?
if (consumedStates.isEmpty() && ourNewStates.isEmpty()) {
log.trace { "tx ${tx.id} was irrelevant to this vault, ignoring" }
return Vault.NoUpdate
}
return Vault.Update(consumedStates, ourNewStates.toHashSet())
}
// TODO : Persists this in DB.
private val authorisedUpgrade = mutableMapOf<StateRef, Class<out UpgradedContract<*, *>>>()

View File

@ -7,6 +7,7 @@ import net.corda.core.flows.StateReplacementException
import net.corda.core.getOrThrow
import net.corda.core.identity.Party
import net.corda.core.node.services.ServiceInfo
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.seconds
import net.corda.node.internal.AbstractNode
@ -106,7 +107,8 @@ class NotaryChangeTests {
val newState = future.resultFuture.getOrThrow()
assertEquals(newState.state.notary, newNotary)
val notaryChangeTx = clientNodeA.services.validatedTransactions.getTransaction(newState.ref.txhash)!!.tx
val recordedTx = clientNodeA.services.validatedTransactions.getTransaction(newState.ref.txhash)!!
val notaryChangeTx = recordedTx.resolveNotaryChangeTransaction(clientNodeA.services)
// Check that all encumbrances have been propagated to the outputs
val originalOutputs = issueTx.outputStates
@ -166,7 +168,7 @@ fun issueState(node: AbstractNode, notaryNode: AbstractNode): StateAndRef<*> {
fun issueMultiPartyState(nodeA: AbstractNode, nodeB: AbstractNode, notaryNode: AbstractNode): StateAndRef<DummyContract.MultiOwnerState> {
val state = TransactionState(DummyContract.MultiOwnerState(0,
listOf(nodeA.info.legalIdentity, nodeB.info.legalIdentity)), notaryNode.info.notaryIdentity)
val tx = TransactionType.NotaryChange.Builder(notaryNode.info.notaryIdentity).withItems(state)
val tx = TransactionBuilder(notary = notaryNode.info.notaryIdentity).withItems(state)
val signedByA = nodeA.services.signInitialTransaction(tx)
val signedByAB = nodeB.services.addSignature(signedByA)
val stx = notaryNode.services.addSignature(signedByAB, notaryNode.services.notaryIdentityKey)

View File

@ -210,7 +210,6 @@ class RequeryConfigurationTest : TestDependencyInjectionBase() {
outputs = emptyList(),
commands = emptyList(),
notary = DUMMY_NOTARY,
signers = emptyList(),
type = TransactionType.General,
timeWindow = null
)

View File

@ -148,7 +148,6 @@ class DBTransactionStorageTests : TestDependencyInjectionBase() {
outputs = emptyList(),
commands = emptyList(),
notary = DUMMY_NOTARY,
signers = emptyList(),
type = TransactionType.General,
timeWindow = null
)

View File

@ -3,6 +3,8 @@
package net.corda.testing
import com.google.common.util.concurrent.Futures
import net.corda.core.contracts.Command
import net.corda.core.contracts.TypeOnlyCommandData
import net.corda.core.crypto.*
import net.corda.core.crypto.testing.DummyPublicKey
import net.corda.core.identity.Party
@ -73,6 +75,8 @@ val DUMMY_CA: CertificateAndKeyPair by lazy {
CertificateAndKeyPair(cert, DUMMY_CA_KEY)
}
fun dummyCommand(vararg signers: PublicKey) = Command<TypeOnlyCommandData>(object : TypeOnlyCommandData() {}, signers.toList())
//
// Extensions to the Driver DSL to auto-manufacture nodes by name.
//

View File

@ -1,12 +1,14 @@
package net.corda.testing
import net.corda.core.contracts.*
import net.corda.core.crypto.*
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.composite.expandedCompositeKeys
import net.corda.core.crypto.sign
import net.corda.core.crypto.testing.NullSignature
import net.corda.core.crypto.toStringShort
import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.serialize
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
@ -14,6 +16,9 @@ import java.io.InputStream
import java.security.KeyPair
import java.security.PublicKey
import java.util.*
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.set
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
@ -329,14 +334,14 @@ data class TestLedgerDSLInterpreter private constructor(
* @return List of [SignedTransaction]s.
*/
fun signAll(transactionsToSign: List<WireTransaction>, extraKeys: List<KeyPair>) = transactionsToSign.map { wtx ->
check(wtx.mustSign.isNotEmpty())
check(wtx.requiredSigningKeys.isNotEmpty())
val signatures = ArrayList<DigitalSignature.WithKey>()
val keyLookup = HashMap<PublicKey, KeyPair>()
(ALL_TEST_KEYS + extraKeys).forEach {
keyLookup[it.public] = it
}
wtx.mustSign.expandedCompositeKeys.forEach {
wtx.requiredSigningKeys.expandedCompositeKeys.forEach {
val key = keyLookup[it] ?: throw IllegalArgumentException("Missing required key for ${it.toStringShort()}")
signatures += key.sign(wtx.id)
}

View File

@ -68,14 +68,12 @@ data class GeneratedLedger(
)
}
attachmentsGenerator.combine(outputsGen, commandsGenerator) { txAttachments, outputs, commands ->
val signers = commands.flatMap { it.first.signers }
val newTransaction = WireTransaction(
emptyList(),
txAttachments.map { it.id },
outputs,
commands.map { it.first },
null,
signers,
TransactionType.General,
null
)
@ -103,14 +101,12 @@ data class GeneratedLedger(
}
val inputsGen = Generator.sampleBernoulli(inputsToChooseFrom)
return inputsGen.combine(attachmentsGenerator, outputsGen, commandsGenerator) { inputs, txAttachments, outputs, commands ->
val signers = commands.flatMap { it.first.signers } + inputNotary.owningKey
val newTransaction = WireTransaction(
inputs.map { it.ref },
txAttachments.map { it.id },
outputs,
commands.map { it.first },
inputNotary,
signers,
TransactionType.General,
null
)
@ -132,45 +128,7 @@ data class GeneratedLedger(
}
/**
* Generates a notary change transaction.
* Invariants:
* * Input notary must be different from the output ones.
* * All other data must stay the same.
*/
fun notaryChangeTransactionGenerator(inputNotary: Party, inputsToChooseFrom: List<StateAndRef<ContractState>>): Generator<Pair<WireTransaction, GeneratedLedger>> {
val newNotaryGen = pickOneOrMaybeNew(identities - inputNotary, partyGenerator)
val inputsGen = Generator.sampleBernoulli(inputsToChooseFrom)
return inputsGen.flatMap { inputs ->
val signers: List<PublicKey> = (inputs.flatMap { it.state.data.participants } + inputNotary).map { it.owningKey }
val outputsGen = Generator.sequence(inputs.map { input -> newNotaryGen.map { TransactionState(input.state.data, it, null) } })
outputsGen.combine(attachmentsGenerator) { outputs, txAttachments ->
val newNotaries = outputs.map { it.notary }
val newTransaction = WireTransaction(
inputs.map { it.ref },
txAttachments.map { it.id },
outputs,
emptyList(),
inputNotary,
signers,
TransactionType.NotaryChange,
null
)
val newOutputStateAndRefs = outputs.mapIndexed { i, state ->
StateAndRef(state, StateRef(newTransaction.id, i))
}
val availableOutputsMinusConsumed = HashMap(availableOutputs)
availableOutputsMinusConsumed[inputNotary] = inputsToChooseFrom - inputs
val newAvailableOutputs = availableOutputsMinusConsumed + newOutputStateAndRefs.groupBy { it.state.notary }
val newAttachments = attachments + txAttachments
val newIdentities = identities + newNotaries
val newLedger = GeneratedLedger(transactions + newTransaction, newAvailableOutputs, newAttachments, newIdentities)
Pair(newTransaction, newLedger)
}
}
}
/**
* Generates a valid transaction. It may be one of three types of issuance, regular and notary change. These have
* Generates a valid transaction. It may be either an issuance or a regular spend transaction. These have
* different invariants on notary fields.
*/
val transactionGenerator: Generator<Pair<WireTransaction, GeneratedLedger>> by lazy {
@ -180,9 +138,8 @@ data class GeneratedLedger(
Generator.pickOne(availableOutputs.keys.toList()).flatMap { inputNotary ->
val inputsToChooseFrom = availableOutputs[inputNotary]!!
Generator.frequency(
0.3 to issuanceGenerator,
0.4 to regularTransactionGenerator(inputNotary, inputsToChooseFrom),
0.3 to notaryChangeTransactionGenerator(inputNotary, inputsToChooseFrom)
0.5 to issuanceGenerator,
0.5 to regularTransactionGenerator(inputNotary, inputsToChooseFrom)
)
}
}