mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
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:
parent
25be649f7b
commit
4487408526
@ -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()
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
finalTx.verifyRequiredSignatures()
|
||||
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 {
|
||||
|
@ -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}"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
newOutputPosition++
|
||||
/** 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)
|
||||
}
|
||||
|
||||
return participants
|
||||
return states
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
stx.verifySignaturesExcept(notaryParty.owningKey)
|
||||
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 {
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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.
|
||||
@ -316,4 +329,4 @@ inline fun <reified T : LinearState> VaultService.linearHeadsOfType() =
|
||||
|
||||
class StatesNotAvailableException(override val message: String?, override val cause: Throwable? = null) : FlowException(message, cause) {
|
||||
override fun toString() = "Soft locking error: $message"
|
||||
}
|
||||
}
|
@ -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) }
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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(
|
||||
/** The inputs of this transaction. Note that in BaseTransaction subclasses the type of this list may change! */
|
||||
open val inputs: List<*>,
|
||||
/** Ordered list of states defined by this transaction, along with the associated notaries. */
|
||||
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 class BaseTransaction : NamedByHash {
|
||||
/** The inputs of this transaction. Note that in BaseTransaction subclasses the type of this list may change! */
|
||||
abstract val inputs: List<*>
|
||||
/** Ordered list of states defined by this transaction, along with the associated notaries. */
|
||||
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.
|
||||
*/
|
||||
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)"
|
||||
}
|
@ -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" }
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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 {
|
||||
// DOCEND 1
|
||||
constructor(wtx: WireTransaction, sigs: List<DigitalSignature.WithKey>) : this(wtx.serialize(), sigs) {
|
||||
cachedTransaction = wtx
|
||||
data class SignedTransaction(val txBits: SerializedBytes<CoreTransaction>,
|
||||
override val sigs: List<DigitalSignature.WithKey>
|
||||
) : TransactionWithSignatures {
|
||||
// DOCEND 1
|
||||
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()}")
|
||||
}
|
||||
|
@ -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()}")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
@ -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>>()) }
|
||||
}
|
||||
}
|
||||
|
@ -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>>())
|
||||
}
|
||||
|
@ -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)) }
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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"
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 {
|
||||
it.verify()
|
||||
it
|
||||
val parts = receive<Any>(otherSide).unwrap {
|
||||
when (it) {
|
||||
is FilteredTransaction -> {
|
||||
it.verify()
|
||||
TransactionParts(it.rootHash, it.filteredLeaves.inputs, it.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 TransactionParts(ftx.rootHash, ftx.filteredLeaves.inputs, ftx.filteredLeaves.timeWindow)
|
||||
return parts
|
||||
}
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
|
@ -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<*, *>>>()
|
||||
|
||||
|
@ -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)
|
||||
|
@ -210,7 +210,6 @@ class RequeryConfigurationTest : TestDependencyInjectionBase() {
|
||||
outputs = emptyList(),
|
||||
commands = emptyList(),
|
||||
notary = DUMMY_NOTARY,
|
||||
signers = emptyList(),
|
||||
type = TransactionType.General,
|
||||
timeWindow = null
|
||||
)
|
||||
|
@ -148,7 +148,6 @@ class DBTransactionStorageTests : TestDependencyInjectionBase() {
|
||||
outputs = emptyList(),
|
||||
commands = emptyList(),
|
||||
notary = DUMMY_NOTARY,
|
||||
signers = emptyList(),
|
||||
type = TransactionType.General,
|
||||
timeWindow = null
|
||||
)
|
||||
|
@ -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.
|
||||
//
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user