Refactor the core transaction types to improve clarity, simplify verification and prepare for sandboxing.

Changes include:
- LedgerTransaction is now much more central: it represents a fully resolved and looked-up tx, with the inputs available.
- TransactionGroup and TransactionForVerification are gone. There is a temporary TransactionForContract class for backwards
  compatibility but it will also be gone soon.
- ResolveTransactionsProtocol is simplified, and now commits a tx to the database as soon as it's determined to be valid.
- ServiceHub is now passed in more consistently to verification code, so we can use more services in future more easily e.g. a sandboxing service.
- A variety of APIs have been tweaked or documented better.
This commit is contained in:
Mike Hearn 2016-07-29 15:52:04 +02:00
parent 918de94a22
commit 701fc853ad
21 changed files with 315 additions and 594 deletions

View File

@ -4,9 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
import com.r3corda.contracts.asset.Cash
import com.r3corda.contracts.asset.sumCashBy
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.signWithECDSA
import com.r3corda.core.crypto.*
import com.r3corda.core.node.NodeInfo
import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.random63BitValue
@ -121,17 +119,17 @@ object TwoPartyTradeProtocol {
progressTracker.nextStep()
// Check that the tx proposed by the buyer is valid.
val missingSigs = it.verify(throwIfSignaturesAreMissing = false)
if (missingSigs != setOf(myKeyPair.public, notaryNode.identity.owningKey))
throw SignatureException("The set of missing signatures is not as expected: $missingSigs")
val missingSigs: Set<PublicKey> = it.verifySignatures(throwIfSignaturesAreMissing = false)
val expected = setOf(myKeyPair.public, notaryNode.identity.owningKey)
if (missingSigs != expected)
throw SignatureException("The set of missing signatures is not as expected: ${missingSigs.toStringsShort()} vs [${myKeyPair.public.toStringShort()}, ${notaryNode.identity.owningKey.toStringShort()}]")
val wtx: WireTransaction = it.tx
logger.trace { "Received partially signed transaction: ${it.id}" }
checkDependencies(it)
// This verifies that the transaction is contract-valid, even though it is missing signatures.
serviceHub.verifyTransaction(wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments))
// Download and check all the things that this transaction depends on and verify it is contract-valid,
// even though it is missing signatures.
subProtocol(ResolveTransactionsProtocol(wtx, otherSide))
if (wtx.outputs.map { it.data }.sumCashBy(myKeyPair.public).withoutIssuer() != price)
throw IllegalArgumentException("Transaction is not sending us the right amount of cash")
@ -150,14 +148,6 @@ object TwoPartyTradeProtocol {
}
}
@Suspendable
private fun checkDependencies(stx: SignedTransaction) {
// Download and check all the transactions that this transaction depends on, but do not check this
// transaction itself.
val dependencyTxIDs = stx.tx.inputs.map { it.txhash }.toSet()
subProtocol(ResolveTransactionsProtocol(dependencyTxIDs, otherSide))
}
open fun signWithOurKey(partialTX: SignedTransaction): DigitalSignature.WithKey {
progressTracker.currentStep = SIGNING
return myKeyPair.signWithECDSA(partialTX.txBits)
@ -206,7 +196,7 @@ object TwoPartyTradeProtocol {
logger.trace { "Got signatures from seller, verifying ... " }
val fullySigned = stx + signatures.sellerSig + signatures.notarySig
fullySigned.verify()
fullySigned.verifySignatures()
logger.trace { "Signatures received are valid. Trade complete! :-)" }
return fullySigned

View File

@ -1,11 +1,12 @@
package com.r3corda.contracts
import com.r3corda.contracts.asset.*
import com.r3corda.contracts.testing.fillWithSomeTestCash
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.days
import com.r3corda.core.node.services.testing.MockStorageService
import com.r3corda.core.node.services.testing.MockServices
import com.r3corda.core.seconds
import com.r3corda.core.testing.*
import org.junit.Test
@ -72,7 +73,6 @@ class CommercialPaperTestsGeneric {
@Parameterized.Parameter
lateinit var thisTest: ICommercialPaperTestTemplate
val attachments = MockStorageService().attachments
val issuer = MEGA_CORP.ref(123)
@Test
@ -190,59 +190,62 @@ class CommercialPaperTestsGeneric {
@Test
fun `issue move and then redeem`() {
// MiniCorp issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself.
val issueTX: LedgerTransaction = run {
val ptx = CommercialPaper().generateIssue(MINI_CORP.ref(123), 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER,
TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply {
val aliceServices = MockServices()
val alicesWallet = aliceServices.fillWithSomeTestCash(9000.DOLLARS)
val bigCorpServices = MockServices()
val bigCorpWallet = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS)
// Propagate the cash transactions to each side.
aliceServices.recordTransactions(bigCorpWallet.states.map { bigCorpServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! })
bigCorpServices.recordTransactions(alicesWallet.states.map { aliceServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! })
// BigCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself.
val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER
val issuance = bigCorpServices.storageService.myLegalIdentity.ref(1)
val issueTX: SignedTransaction =
CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply {
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
signWith(MINI_CORP_KEY)
signWith(bigCorpServices.key)
signWith(DUMMY_NOTARY_KEY)
}
val stx = ptx.toSignedTransaction()
stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments)
}
}.toSignedTransaction()
val (alicesWalletTX, alicesWallet) = cashOutputsToWallet(
3000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY,
3000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY,
3000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY
)
// Alice pays $9000 to MiniCorp to own some of their debt.
val moveTX: LedgerTransaction = run {
// Alice pays $9000 to BigCorp to own some of their debt.
val moveTX: SignedTransaction = run {
val ptx = TransactionType.General.Builder()
Cash().generateSpend(ptx, 9000.DOLLARS, MINI_CORP_PUBKEY, alicesWallet)
CommercialPaper().generateMove(ptx, issueTX.outRef(0), ALICE_PUBKEY)
ptx.signWith(MINI_CORP_KEY)
ptx.signWith(ALICE_KEY)
Cash().generateSpend(ptx, 9000.DOLLARS, bigCorpServices.key.public, alicesWallet.statesOfType<Cash.State>())
CommercialPaper().generateMove(ptx, issueTX.tx.outRef(0), aliceServices.key.public)
ptx.signWith(bigCorpServices.key)
ptx.signWith(aliceServices.key)
ptx.signWith(DUMMY_NOTARY_KEY)
ptx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments)
ptx.toSignedTransaction()
}
// Won't be validated.
val (corpWalletTX, corpWallet) = cashOutputsToWallet(
9000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` MINI_CORP_PUBKEY `with notary` DUMMY_NOTARY,
4000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` MINI_CORP_PUBKEY `with notary` DUMMY_NOTARY
)
fun makeRedeemTX(time: Instant): LedgerTransaction {
fun makeRedeemTX(time: Instant): SignedTransaction {
val ptx = TransactionType.General.Builder()
ptx.setTime(time, DUMMY_NOTARY, 30.seconds)
CommercialPaper().generateRedeem(ptx, moveTX.outRef(1), corpWallet)
ptx.signWith(ALICE_KEY)
ptx.signWith(MINI_CORP_KEY)
CommercialPaper().generateRedeem(ptx, moveTX.tx.outRef(1), bigCorpWallet.statesOfType<Cash.State>())
ptx.signWith(aliceServices.key)
ptx.signWith(bigCorpServices.key)
ptx.signWith(DUMMY_NOTARY_KEY)
return ptx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments)
return ptx.toSignedTransaction()
}
val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days)
val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days)
// Verify the txns are valid and insert into both sides.
listOf(issueTX, moveTX).forEach {
it.toLedgerTransaction(aliceServices).verify()
aliceServices.recordTransactions(it)
bigCorpServices.recordTransactions(it)
}
val e = assertFailsWith(TransactionVerificationException::class) {
TransactionGroup(setOf(issueTX, moveTX, tooEarlyRedemption), setOf(corpWalletTX, alicesWalletTX)).verify()
tooEarlyRedemption.toLedgerTransaction(aliceServices).verify()
}
assertTrue(e.cause!!.message!!.contains("paper must have matured"))
TransactionGroup(setOf(issueTX, moveTX, validRedemption), setOf(corpWalletTX, alicesWalletTX)).verify()
validRedemption.toLedgerTransaction(aliceServices).verify()
}
}

View File

@ -1,7 +1,7 @@
package com.r3corda.contracts
import com.r3corda.core.contracts.*
import com.r3corda.core.node.services.testing.MockStorageService
import com.r3corda.core.node.services.testing.MockServices
import com.r3corda.core.seconds
import com.r3corda.core.testing.*
import org.junit.Test
@ -195,9 +195,6 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State {
}
class IRSTests {
val attachments = MockStorageService().attachments
@Test
fun ok() {
trade().verifies()
@ -211,9 +208,9 @@ class IRSTests {
/**
* Generate an IRS txn - we'll need it for a few things.
*/
fun generateIRSTxn(irsSelect: Int): LedgerTransaction {
fun generateIRSTxn(irsSelect: Int): SignedTransaction {
val dummyIRS = createDummyIRS(irsSelect)
val genTX: LedgerTransaction = run {
val genTX: SignedTransaction = run {
val gtx = InterestRateSwap().generateAgreement(
fixedLeg = dummyIRS.fixedLeg,
floatingLeg = dummyIRS.floatingLeg,
@ -225,7 +222,7 @@ class IRSTests {
signWith(MINI_CORP_KEY)
signWith(DUMMY_NOTARY_KEY)
}
gtx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments)
gtx.toSignedTransaction()
}
return genTX
}
@ -243,7 +240,7 @@ class IRSTests {
* Utility so I don't have to keep typing this.
*/
fun singleIRS(irsSelector: Int = 1): InterestRateSwap.State {
return generateIRSTxn(irsSelector).outputs.map { it.data }.filterIsInstance<InterestRateSwap.State>().single()
return generateIRSTxn(irsSelector).tx.outputs.map { it.data }.filterIsInstance<InterestRateSwap.State>().single()
}
/**
@ -293,30 +290,30 @@ class IRSTests {
*/
@Test
fun generateIRSandFixSome() {
val services = MockServices()
var previousTXN = generateIRSTxn(1)
fun currentIRS() = previousTXN.outputs.map { it.data }.filterIsInstance<InterestRateSwap.State>().single()
val txns = HashSet<LedgerTransaction>()
txns += previousTXN
previousTXN.toLedgerTransaction(services).verify()
services.recordTransactions(previousTXN)
fun currentIRS() = previousTXN.tx.outputs.map { it.data }.filterIsInstance<InterestRateSwap.State>().single()
while (true) {
val nextFix: FixOf = currentIRS().nextFixingOf() ?: break
val fixTX: LedgerTransaction = run {
val fixTX: SignedTransaction = run {
val tx = TransactionType.General.Builder()
val fixing = Fix(nextFix, "0.052".percent.value)
InterestRateSwap().generateFix(tx, previousTXN.outRef(0), fixing)
InterestRateSwap().generateFix(tx, previousTXN.tx.outRef(0), fixing)
with(tx) {
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
signWith(MEGA_CORP_KEY)
signWith(MINI_CORP_KEY)
signWith(DUMMY_NOTARY_KEY)
}
tx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments)
tx.toSignedTransaction()
}
fixTX.toLedgerTransaction(services).verify()
services.recordTransactions(fixTX)
previousTXN = fixTX
txns += fixTX
}
TransactionGroup(txns, emptySet()).verify()
}
// Move these later as they aren't IRS specific.

View File

@ -289,7 +289,7 @@ class ObligationTests {
}.toSignedTransaction()
assertEquals(1, tx.tx.outputs.size)
assertEquals(stateAndRef.state.data.copy(lifecycle = Lifecycle.DEFAULTED), tx.tx.outputs[0].data)
assertTrue(tx.verify().isEmpty())
tx.verifySignatures()
// And set it back
stateAndRef = tx.tx.outRef<Obligation.State<Currency>>(0)
@ -300,7 +300,7 @@ class ObligationTests {
}.toSignedTransaction()
assertEquals(1, tx.tx.outputs.size)
assertEquals(stateAndRef.state.data.copy(lifecycle = Lifecycle.NORMAL), tx.tx.outputs[0].data)
assertTrue(tx.verify().isEmpty())
tx.verifySignatures()
}
/** Test generating a transaction to settle an obligation. */

View File

@ -1,32 +1,39 @@
package com.r3corda.core.contracts
import com.r3corda.core.node.services.AttachmentStorage
import com.r3corda.core.node.services.IdentityService
import com.r3corda.core.node.ServiceHub
import java.io.FileNotFoundException
// TODO: Move these into the actual classes (i.e. where people would expect to find them) and split Transactions.kt into multiple files
/**
* Looks up identities and attachments from storage to generate a [LedgerTransaction].
* Looks up identities and attachments from storage to generate a [LedgerTransaction]. A transaction is expected to
* have been fully resolved using the resolution protocol by this point.
*
* @throws FileNotFoundException if a required attachment was not found in storage.
* @throws TransactionResolutionException if an input points to a transaction not found in storage.
*/
fun WireTransaction.toLedgerTransaction(identityService: IdentityService,
attachmentStorage: AttachmentStorage): LedgerTransaction {
fun WireTransaction.toLedgerTransaction(services: ServiceHub): LedgerTransaction {
// Look up random keys to authenticated identities. This is just a stub placeholder and will all change in future.
val authenticatedArgs = commands.map {
val institutions = it.signers.mapNotNull { pk -> identityService.partyFromKey(pk) }
AuthenticatedObject(it.signers, institutions, it.value)
val parties = it.signers.mapNotNull { pk -> services.identityService.partyFromKey(pk) }
AuthenticatedObject(it.signers, parties, it.value)
}
// Open attachments specified in this transaction. If we haven't downloaded them, we fail.
val attachments = attachments.map {
attachmentStorage.openAttachment(it) ?: throw FileNotFoundException(it.toString())
services.storageService.attachments.openAttachment(it) ?: throw FileNotFoundException(it.toString())
}
return LedgerTransaction(inputs, outputs, authenticatedArgs, attachments, id, signers, type)
val resolvedInputs = inputs.map { StateAndRef(services.loadState(it), it) }
return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, signers, type)
}
/**
* Calls [verify] to check all required signatures are present, and then uses the passed [IdentityService] to call
* [WireTransaction.toLedgerTransaction] to look up well known identities from pubkeys.
* Calls [verify] to check all required signatures are present, and then calls [WireTransaction.toLedgerTransaction]
* with the passed in [ServiceHub] to resolve the dependencies, returning an unverified LedgerTransaction.
*
* @throws FileNotFoundException if a required attachment was not found in storage.
* @throws TransactionResolutionException if an input points to a transaction not found in storage.
*/
fun SignedTransaction.verifyToLedgerTransaction(identityService: IdentityService,
attachmentStorage: AttachmentStorage): LedgerTransaction {
verify()
return tx.toLedgerTransaction(identityService, attachmentStorage)
fun SignedTransaction.toLedgerTransaction(services: ServiceHub): LedgerTransaction {
verifySignatures()
return tx.toLedgerTransaction(services)
}

View File

@ -16,18 +16,17 @@ sealed class TransactionType {
*
* Note: Presence of _signatures_ is not checked, only the public keys to be signed for.
*/
fun verify(tx: TransactionForVerification) {
fun verify(tx: LedgerTransaction) {
val missing = verifySigners(tx)
if (missing.isNotEmpty()) throw TransactionVerificationException.SignersMissing(tx, missing.toList())
verifyTransaction(tx)
}
/** Check that the list of signers includes all the necessary keys */
fun verifySigners(tx: TransactionForVerification): Set<PublicKey> {
fun verifySigners(tx: LedgerTransaction): Set<PublicKey> {
val timestamp = tx.commands.noneOrSingle { it.value is TimestampCommand }
val timestampKey = timestamp?.signers.orEmpty()
val notaryKey = (tx.inputs.map { it.notary.owningKey } + timestampKey).toSet()
val notaryKey = (tx.inputs.map { it.state.notary.owningKey } + timestampKey).toSet()
if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx)
val requiredKeys = getRequiredSigners(tx) + notaryKey
@ -40,10 +39,10 @@ sealed class TransactionType {
* Return the list of public keys that that require signatures for the transaction type.
* Note: the notary key is checked separately for all transactions and need not be included.
*/
abstract fun getRequiredSigners(tx: TransactionForVerification): Set<PublicKey>
abstract fun getRequiredSigners(tx: LedgerTransaction): Set<PublicKey>
/** Implement type specific transaction validation logic */
abstract fun verifyTransaction(tx: TransactionForVerification)
abstract fun verifyTransaction(tx: LedgerTransaction)
/** A general transaction type where transaction validity is determined by custom contract code */
class General : TransactionType() {
@ -54,10 +53,11 @@ sealed class TransactionType {
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
* If any contract fails to verify, the whole transaction is considered to be invalid.
*/
override fun verifyTransaction(tx: TransactionForVerification) {
override fun verifyTransaction(tx: LedgerTransaction) {
// TODO: Check that notary is unchanged
val ctx = tx.toTransactionForContract()
// TODO: This will all be replaced in future once the sandbox and contract constraints work is done.
val contracts = (ctx.inputs.map { it.contract } + ctx.outputs.map { it.contract }).toSet()
for (contract in contracts) {
try {
@ -68,10 +68,7 @@ sealed class TransactionType {
}
}
override fun getRequiredSigners(tx: TransactionForVerification): Set<PublicKey> {
val commandKeys = tx.commands.flatMap { it.signers }.toSet()
return commandKeys
}
override fun getRequiredSigners(tx: LedgerTransaction) = tx.commands.flatMap { it.signers }.toSet()
}
/**
@ -91,14 +88,16 @@ sealed class TransactionType {
}
/**
* Check that the difference between inputs and outputs is only the notary field,
* and that all required signing public keys are present.
* 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: TransactionForVerification) {
override fun verifyTransaction(tx: LedgerTransaction) {
try {
tx.inputs.zip(tx.outputs).forEach {
check(it.first.data == it.second.data)
check(it.first.notary != it.second.notary)
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) {
@ -106,9 +105,6 @@ sealed class TransactionType {
}
}
override fun getRequiredSigners(tx: TransactionForVerification): Set<PublicKey> {
val participantKeys = tx.inputs.flatMap { it.data.participants }.toSet()
return participantKeys
}
override fun getRequiredSigners(tx: LedgerTransaction) = tx.inputs.flatMap { it.state.data.participants }.toSet()
}
}

View File

@ -8,50 +8,6 @@ import java.util.*
// TODO: Consider moving this out of the core module and providing a different way for unit tests to test contracts.
/**
* A TransactionGroup defines a directed acyclic graph of transactions that can be resolved with each other and then
* verified. Successful verification does not imply the non-existence of other conflicting transactions: simply that
* this subgraph does not contain conflicts and is accepted by the involved contracts.
*
* The inputs of the provided transactions must be resolvable either within the [transactions] set, or from the
* [nonVerifiedRoots] set. Transactions in the non-verified set are ignored other than for looking up input states.
*/
class TransactionGroup(val transactions: Set<LedgerTransaction>, val nonVerifiedRoots: Set<LedgerTransaction>) {
/**
* Verifies the group and returns the set of resolved transactions.
*/
fun verify(): Set<TransactionForVerification> {
// Check that every input can be resolved to an output.
// Check that no output is referenced by more than one input.
// Cycles should be impossible due to the use of hashes as pointers.
check(transactions.intersect(nonVerifiedRoots).isEmpty())
val hashToTXMap: Map<SecureHash, List<LedgerTransaction>> = (transactions + nonVerifiedRoots).groupBy { it.id }
val refToConsumingTXMap = hashMapOf<StateRef, LedgerTransaction>()
val resolved = HashSet<TransactionForVerification>(transactions.size)
for (tx in transactions) {
val inputs = ArrayList<TransactionState<ContractState>>(tx.inputs.size)
for (ref in tx.inputs) {
val conflict = refToConsumingTXMap[ref]
if (conflict != null)
throw TransactionConflictException(ref, tx, conflict)
refToConsumingTXMap[ref] = tx
// Look up the connecting transaction.
val ltx = hashToTXMap[ref.txhash]?.single() ?: throw TransactionResolutionException(ref.txhash)
// Look up the output in that transaction by index.
inputs.add(ltx.outputs[ref.index])
}
resolved.add(TransactionForVerification(inputs, tx.outputs, tx.attachments, tx.commands, tx.id, tx.signers, tx.type))
}
for (tx in resolved)
tx.verify()
return resolved
}
}
/** A transaction in fully resolved and sig-checked form, ready for passing as input to a verification function. */
data class TransactionForVerification(val inputs: List<TransactionState<ContractState>>,
val outputs: List<TransactionState<ContractState>>,
@ -62,20 +18,6 @@ data class TransactionForVerification(val inputs: List<TransactionState<Contract
val type: TransactionType) {
override fun hashCode() = origHash.hashCode()
override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash
/**
* Verifies that the transaction is valid by running type-specific validation logic.
*
* TODO: Move this out of the core data structure definitions, once unit tests are more cleanly separated.
*
* @throws TransactionVerificationException if validation logic fails or if a contract throws an exception
* (the original is in the cause field).
*/
@Throws(TransactionVerificationException::class)
fun verify() = type.verify(this)
fun toTransactionForContract() = TransactionForContract(inputs.map { it.data }, outputs.map { it.data },
attachments, commands, origHash, inputs.map { it.notary }.singleOrNull())
}
/**
@ -171,14 +113,16 @@ data class TransactionForContract(val inputs: List<ContractState>,
fun getTimestampByName(vararg authorityName: String): TimestampCommand? = commands.getTimestampByName(*authorityName)
}
class TransactionResolutionException(val hash: SecureHash) : Exception()
class TransactionResolutionException(val hash: SecureHash) : Exception() {
override fun toString() = "Transaction resolution failure for $hash"
}
class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception()
sealed class TransactionVerificationException(val tx: TransactionForVerification, cause: Throwable?) : Exception(cause) {
class ContractRejection(tx: TransactionForVerification, val contract: Contract, cause: Throwable?) : TransactionVerificationException(tx, cause)
class MoreThanOneNotary(tx: TransactionForVerification) : TransactionVerificationException(tx, null)
class SignersMissing(tx: TransactionForVerification, val missing: List<PublicKey>) : TransactionVerificationException(tx, null) {
sealed class TransactionVerificationException(val tx: LedgerTransaction, cause: Throwable?) : Exception(cause) {
class ContractRejection(tx: LedgerTransaction, val contract: Contract, cause: Throwable?) : TransactionVerificationException(tx, cause)
class MoreThanOneNotary(tx: LedgerTransaction) : TransactionVerificationException(tx, null)
class SignersMissing(tx: LedgerTransaction, val missing: List<PublicKey>) : TransactionVerificationException(tx, null) {
override fun toString() = "Signers missing: ${missing.map { it.toStringShort() }}"
}
class InvalidNotaryChange(tx: TransactionForVerification) : TransactionVerificationException(tx, null)
class InvalidNotaryChange(tx: LedgerTransaction) : TransactionVerificationException(tx, null)
}

View File

@ -21,17 +21,23 @@ import java.security.SignatureException
* a public key that is mentioned inside a transaction command. SignedTransaction is the top level transaction type
* and the type most frequently passed around the network and stored. The identity of a transaction is the hash
* of a WireTransaction, therefore if you are storing data keyed by WT hash be aware that multiple different SWTs may
* map to the same key (and they could be different in important ways!).
* map to the same key (and they could be different in important ways, like validity!). The signatures on a
* SignedTransaction might be invalid or missing: the type does not imply validity.
*
* WireTransaction is a transaction in a form ready to be serialised/unserialised. A WireTransaction can be hashed
* in various ways to calculate a *signature hash* (or sighash), this is the hash that is signed by the various involved
* keypairs. Note that a sighash is not the same thing as a *transaction id*, which is the hash of the entire
* WireTransaction i.e. the outermost serialised form with everything included.
* keypairs. A WireTransaction can be safely deserialised from inside a SignedTransaction outside of the sandbox,
* because it consists of only platform defined types. Any user defined object graphs are kept as byte arrays. A
* WireTransaction may be invalid and missing its dependencies (other transactions + attachments).
*
* LedgerTransaction is derived from WireTransaction. It is the result of doing some basic key lookups on WireCommand
* to see if any keys are from a recognised party, thus converting the WireCommand objects into
* AuthenticatedObject<Command>. Currently we just assume a hard coded pubkey->party map. In future it'd make more
* sense to use a certificate scheme and so that logic would get more complex.
* LedgerTransaction is derived from WireTransaction. It is the result of doing the following operations:
*
* - Downloading and locally storing all the dependencies of the transaction.
* - Doing some basic key lookups on WireCommand to see if any keys are from a recognised party, thus converting the
* WireCommand objects into AuthenticatedObject<Command>. Currently we just assume a hard coded pubkey->party map.
* In future it'd make more sense to use a certificate scheme and so that logic would get more complex.
* - Deserialising the included states, sandboxing the contract classes, and generally ensuring the embedded code is
* safe for interaction.
*
* All the above refer to inputs using a (txhash, output index) pair.
*
@ -73,7 +79,7 @@ data class WireTransaction(val inputs: List<StateRef>,
override fun toString(): String {
val buf = StringBuilder()
buf.appendln("Transaction:")
buf.appendln("Transaction $id:")
for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input")
for (output in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $output")
for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command")
@ -97,18 +103,6 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
/** A transaction ID is the hash of the [WireTransaction]. Thus adding or removing a signature does not change it. */
override val id: SecureHash get() = txBits.hash
/**
* Verifies the given signatures against the serialized transaction data. Does NOT deserialise or check the contents
* to ensure there are no missing signatures: use verify() to do that. This weaker version can be useful for
* checking a partially signed transaction being prepared by multiple co-operating parties.
*
* @throws SignatureException if the signature is invalid or does not match.
*/
fun verifySignatures() {
for (sig in sigs)
sig.verifyWithECDSA(txBits.bits)
}
/**
* Verify the signatures, deserialise the wire transaction and then check that the set of signatures found contains
* the set of pubkeys in the signers list. If any signatures are missing, either throws an exception (by default) or
@ -116,9 +110,12 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
*
* @throws SignatureException if a signature is invalid, does not match or if any signature is missing.
*/
fun verify(throwIfSignaturesAreMissing: Boolean = true): Set<PublicKey> {
verifySignatures()
fun verifySignatures(throwIfSignaturesAreMissing: Boolean = true): Set<PublicKey> {
// Embedded WireTransaction is not deserialised until after we check the signatures.
for (sig in sigs)
sig.verifyWithECDSA(txBits.bits)
// Now examine the contents and ensure the sigs we have line up with the advertised list of signers.
val missing = getMissingSignatures()
if (missing.isNotEmpty() && throwIfSignaturesAreMissing)
throw SignatureException("Missing signatures on transaction ${id.prefixChars()} for: ${missing.map { it.toStringShort() }}")
@ -157,12 +154,10 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
* A LedgerTransaction wraps the data needed to calculate one or more successor states from a set of input states.
* It is the first step after extraction from a WireTransaction. The signatures at this point have been lined up
* with the commands from the wire, and verified/looked up.
*
* TODO: This class needs a bit more thought. Should inputs be fully resolved by this point too?
*/
data class LedgerTransaction(
/** The input states which will be consumed/invalidated by the execution of this transaction. */
val inputs: List<StateRef>,
val inputs: List<StateAndRef<*>>,
/** The states that will be generated by the execution of this transaction. */
val outputs: List<TransactionState<*>>,
/** Arbitrary data passed to the program of each input state. */
@ -171,9 +166,28 @@ data class LedgerTransaction(
val attachments: List<Attachment>,
/** The hash of the original serialised WireTransaction */
override val id: SecureHash,
/** The notary key and the command keys together: a signed transaction must provide signatures for all of these. */
val signers: List<PublicKey>,
val type: TransactionType
) : NamedByHash {
@Suppress("UNCHECKED_CAST")
fun <T : ContractState> outRef(index: Int) = StateAndRef(outputs[index] as TransactionState<T>, StateRef(id, index))
// TODO: Remove this concept.
// There isn't really a good justification for hiding this data from the contract, it's just a backwards compat hack.
/** Strips the transaction down to a form that is usable by the contract verify functions */
fun toTransactionForContract(): TransactionForContract {
return TransactionForContract(inputs.map { it.state.data }, outputs.map { it.data }, attachments, commands, id,
inputs.map { it.state.notary }.singleOrNull())
}
/**
* 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.
*
* @throws TransactionVerificationException if anything goes wrong.
*/
fun verify() = type.verify(this)
}

View File

@ -1,7 +1,10 @@
package com.r3corda.core.node
import com.google.common.util.concurrent.ListenableFuture
import com.r3corda.core.contracts.*
import com.r3corda.core.contracts.SignedTransaction
import com.r3corda.core.contracts.StateRef
import com.r3corda.core.contracts.TransactionResolutionException
import com.r3corda.core.contracts.TransactionState
import com.r3corda.core.messaging.MessagingService
import com.r3corda.core.node.services.*
import com.r3corda.core.protocols.ProtocolLogic
@ -25,19 +28,6 @@ interface ServiceHub {
val schedulerService: SchedulerService
val clock: Clock
/**
* Given a [LedgerTransaction], looks up all its dependencies in the local database, uses the identity service to map
* the [SignedTransaction]s the DB gives back into [LedgerTransaction]s, and then runs the smart contracts for the
* transaction. If no exception is thrown, the transaction is valid.
*/
fun verifyTransaction(ltx: LedgerTransaction) {
val dependencies = ltx.inputs.map {
storageService.validatedTransactions.getTransaction(it.txhash) ?: throw TransactionResolutionException(it.txhash)
}
val ltxns = dependencies.map { it.verifyToLedgerTransaction(identityService, storageService.attachments) }
TransactionGroup(setOf(ltx), ltxns.toSet()).verify()
}
/**
* Given a list of [SignedTransaction]s, writes them to the local storage for validated transactions and then
* sends them to the wallet for further processing.

View File

@ -1,10 +1,7 @@
package com.r3corda.core.testing
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.DigitalSignature
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.signWithECDSA
import com.r3corda.core.crypto.*
import com.r3corda.core.node.ServiceHub
import com.r3corda.core.serialization.serialize
import java.io.InputStream
@ -133,8 +130,7 @@ data class TestTransactionDSLInterpreter private constructor(
}
override fun verifies(): EnforceVerifyOrFail {
val resolvedTransaction = ledgerInterpreter.resolveWireTransaction(toWireTransaction())
resolvedTransaction.verify()
toWireTransaction().toLedgerTransaction(services).verify()
return EnforceVerifyOrFail.Token
}
@ -185,26 +181,6 @@ data class TestLedgerDSLInterpreter private constructor (
nonVerifiedTransactionWithLocations = HashMap(nonVerifiedTransactionWithLocations)
)
internal fun resolveWireTransaction(wireTransaction: WireTransaction): TransactionForVerification {
return wireTransaction.run {
val authenticatedCommands = commands.map {
AuthenticatedObject(it.signers, it.signers.mapNotNull { services.identityService.partyFromKey(it) }, it.value)
}
val resolvedInputStates = inputs.map { resolveStateRef<ContractState>(it) }
val resolvedAttachments = attachments.map { resolveAttachment(it) }
TransactionForVerification(
inputs = resolvedInputStates,
outputs = outputs,
commands = authenticatedCommands,
origHash = wireTransaction.serialized.hash,
attachments = resolvedAttachments,
signers = signers.toList(),
type = type
)
}
}
internal inline fun <reified S : ContractState> resolveStateRef(stateRef: StateRef): TransactionState<S> {
val transactionWithLocation =
transactionWithLocations[stateRef.txhash] ?:
@ -230,16 +206,6 @@ data class TestLedgerDSLInterpreter private constructor (
return transactionInterpreter
}
fun toTransactionGroup(): TransactionGroup {
val ledgerTransactions = transactionWithLocations.map {
it.value.transaction.toLedgerTransaction(services.identityService, services.storageService.attachments)
}
val nonVerifiedLedgerTransactions = nonVerifiedTransactionWithLocations.map {
it.value.transaction.toLedgerTransaction(services.identityService, services.storageService.attachments)
}
return TransactionGroup(ledgerTransactions.toSet(), nonVerifiedLedgerTransactions.toSet())
}
fun transactionName(transactionHash: SecureHash): String? {
val transactionWithLocation = transactionWithLocations[transactionHash]
return if (transactionWithLocation != null) {
@ -298,16 +264,18 @@ data class TestLedgerDSLInterpreter private constructor (
}
override fun verifies(): EnforceVerifyOrFail {
val transactionGroup = toTransactionGroup()
try {
transactionGroup.verify()
services.recordTransactions(transactionsUnverified.map { SignedTransaction(it.serialized, listOf(NullSignature)) })
for ((key, value) in transactionWithLocations) {
value.transaction.toLedgerTransaction(services).verify()
services.recordTransactions(SignedTransaction(value.transaction.serialized, listOf(NullSignature)))
}
return EnforceVerifyOrFail.Token
} catch (exception: TransactionVerificationException) {
val transactionWithLocation = transactionWithLocations[exception.tx.origHash]
val transactionWithLocation = transactionWithLocations[exception.tx.id]
val transactionName = transactionWithLocation?.label ?: transactionWithLocation?.location ?: "<unknown>"
throw VerifiesFailed(transactionName, exception)
}
return EnforceVerifyOrFail.Token
}
override fun <S : ContractState> retrieveOutputStateAndRef(clazz: Class<S>, label: String): StateAndRef<S> {

View File

@ -10,9 +10,8 @@ import com.r3corda.core.node.NodeInfo
import com.r3corda.core.protocols.ProtocolLogic
import com.r3corda.core.random63BitValue
import com.r3corda.core.utilities.ProgressTracker
import com.r3corda.protocols.NotaryProtocol
import com.r3corda.protocols.PartyRequestMessage
import com.r3corda.protocols.ResolveTransactionsProtocol
import com.r3corda.protocols.AbstractStateReplacementProtocol.Acceptor
import com.r3corda.protocols.AbstractStateReplacementProtocol.Instigator
import java.security.PublicKey
/**
@ -164,13 +163,14 @@ abstract class AbstractStateReplacementProtocol<T> {
val response = Result.noError(mySignature)
val swapSignatures = sendAndReceive<List<DigitalSignature.WithKey>>(otherSide, sessionIdForSend, sessionIdForReceive, response)
// TODO: This step should not be necessary, as signatures are re-checked in verifySignatures.
val allSignatures = swapSignatures.validate { signatures ->
signatures.forEach { it.verifyWithECDSA(stx.txBits) }
signatures
}
val finalTx = stx + allSignatures
finalTx.verify()
finalTx.verifySignatures()
serviceHub.recordTransactions(listOf(finalTx))
}
@ -191,7 +191,9 @@ abstract class AbstractStateReplacementProtocol<T> {
private fun verifyTx(stx: SignedTransaction) {
checkMySignatureRequired(stx.tx)
checkDependenciesValid(stx)
checkValid(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()
}
private fun checkMySignatureRequired(tx: WireTransaction) {
@ -202,19 +204,10 @@ abstract class AbstractStateReplacementProtocol<T> {
@Suspendable
private fun checkDependenciesValid(stx: SignedTransaction) {
val dependencyTxIDs = stx.tx.inputs.map { it.txhash }.toSet()
subProtocol(ResolveTransactionsProtocol(dependencyTxIDs, otherSide))
subProtocol(ResolveTransactionsProtocol(stx.tx, otherSide))
}
private fun checkValid(stx: SignedTransaction) {
val ltx = stx.tx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
serviceHub.verifyTransaction(ltx)
}
private fun sign(stx: SignedTransaction): DigitalSignature.WithKey {
val myKeyPair = serviceHub.storageService.myLegalIdentityKey
return myKeyPair.signWithECDSA(stx.txBits)
}
private fun sign(stx: SignedTransaction) = serviceHub.storageService.myLegalIdentityKey.signWithECDSA(stx.txBits)
}
// TODO: similar classes occur in other places (NotaryProtocol), need to consolidate

View File

@ -1,27 +1,35 @@
package com.r3corda.protocols
import co.paralleluniverse.fibers.Suspendable
import com.r3corda.core.contracts.*
import com.r3corda.core.checkedAdd
import com.r3corda.core.contracts.LedgerTransaction
import com.r3corda.core.contracts.SignedTransaction
import com.r3corda.core.contracts.WireTransaction
import com.r3corda.core.contracts.toLedgerTransaction
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.protocols.ProtocolLogic
import java.util.*
// NB: This code is unit tested by TwoPartyTradeProtocolTests
// TODO: This code is currently unit tested by TwoPartyTradeProtocolTests, 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 protocol fetches each transaction identified by the given hashes from either disk or network, along with all
* their dependencies, and verifies them together using a single [TransactionGroup]. If no exception is thrown, then
* all the transactions have been successfully verified and inserted into the local database.
* This protocol 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.
*
* 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 inserted. This way to use the
* protocol is helpful when resolving and verifying a finished but partially signed transaction.
* then this isn't enough to put into the local database, so only the dependencies are checked and inserted. This way
* to use the protocol is helpful when resolving and verifying a finished but partially signed transaction.
*
* The protocol returns a list of verified [LedgerTransaction] objects, in a depth-first order.
*/
class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
private val otherSide: Party) : ProtocolLogic<Unit>() {
private val otherSide: Party) : ProtocolLogic<List<LedgerTransaction>>() {
companion object {
private fun dependencyIDs(wtx: WireTransaction) = wtx.inputs.map { it.txhash }.toSet()
@ -48,45 +56,49 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
}
@Suspendable
override fun call(): Unit {
val toVerify = HashSet<LedgerTransaction>()
val alreadyVerified = HashSet<LedgerTransaction>()
val downloadedSignedTxns = ArrayList<SignedTransaction>()
override fun call(): List<LedgerTransaction> {
val newTxns: Iterable<SignedTransaction> = downloadDependencies(txHashes)
// This fills out toVerify, alreadyVerified (roots) and downloadedSignedTxns.
fetchDependenciesAndCheckSignatures(txHashes, toVerify, alreadyVerified, downloadedSignedTxns)
// 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>()
if (stx != null) {
// Check the signatures on the stx first.
toVerify += stx!!.verifyToLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
} else if (wtx != null) {
wtx!!.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
for (tx in newTxns) {
// Resolve to a LedgerTransaction and then run all contracts.
val ltx = tx.toLedgerTransaction(serviceHub)
ltx.verify()
serviceHub.recordTransactions(tx)
result += ltx
}
// Run all the contracts and throw an exception if any of them reject.
TransactionGroup(toVerify, alreadyVerified).verify()
// If this protocol 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.
stx?.let {
fetchMissingAttachments(listOf(it.tx))
val ltx = it.toLedgerTransaction(serviceHub)
ltx.verify()
result += ltx
}
wtx?.let {
fetchMissingAttachments(listOf(it))
val ltx = it.toLedgerTransaction(serviceHub)
ltx.verify()
result += ltx
}
// Now write all the transactions we just validated back to the database for next time, including
// signatures so we can serve up these transactions to other peers when we, in turn, send one that
// depends on them onto another peer.
//
// It may seem tempting to write transactions to the database as we receive them, instead of all at once
// here at the end. Doing it this way avoids cases where a transaction is in the database but its
// dependencies aren't, or an unvalidated and possibly broken tx is there.
serviceHub.recordTransactions(downloadedSignedTxns)
return result
}
override val topic: String get() = throw UnsupportedOperationException()
@Suspendable
private fun fetchDependenciesAndCheckSignatures(depsToCheck: Set<SecureHash>,
toVerify: HashSet<LedgerTransaction>,
alreadyVerified: HashSet<LedgerTransaction>,
downloadedSignedTxns: ArrayList<SignedTransaction>) {
// Maintain a work queue of all hashes to load/download, initialised with our starting set.
// Then either fetch them from the database or request them from the other side. Look up the
// signatures against our identity database, filtering the transactions into 'already checked'
// and 'need to check' sets.
private fun downloadDependencies(depsToCheck: Set<SecureHash>): List<SignedTransaction> {
// Maintain a work queue of all hashes to load/download, initialised with our starting set. Then do a breadth
// first traversal across the dependency graph.
//
// TODO: This approach has two problems. Analyze and resolve them:
//
@ -103,45 +115,49 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
val nextRequests = LinkedHashSet<SecureHash>() // Keep things unique but ordered, for unit test stability.
nextRequests.addAll(depsToCheck)
val resultQ = LinkedHashMap<SecureHash, SignedTransaction>()
var limitCounter = 0
while (nextRequests.isNotEmpty()) {
val (fromDisk, downloads) = subProtocol(FetchTransactionsProtocol(nextRequests, otherSide))
// Don't re-download the same tx when we haven't verified it yet but it's referenced multiple times in the
// graph we're traversing.
val notAlreadyFetched = nextRequests.filterNot { it in resultQ }.toSet()
nextRequests.clear()
// TODO: This could be done in parallel with other fetches for extra speed.
resolveMissingAttachments(downloads)
if (notAlreadyFetched.isEmpty()) // Done early.
break
// Resolve any legal identities from known public keys in the signatures.
val downloadedTxns = downloads.map {
it.verifyToLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
}
// Request the standalone transaction data (which may refer to things we don't yet have).
val downloads: List<SignedTransaction> = subProtocol(FetchTransactionsProtocol(notAlreadyFetched, otherSide)).downloaded
// Do the same for transactions loaded from disk (i.e. we checked them previously).
val loadedTxns = fromDisk.map {
it.verifyToLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
}
fetchMissingAttachments(downloads.map { it.tx })
toVerify.addAll(downloadedTxns)
alreadyVerified.addAll(loadedTxns)
downloadedSignedTxns.addAll(downloads)
for (stx in downloads)
check(resultQ.putIfAbsent(stx.id, stx) == null) // Assert checks the filter at the start.
// And now add all the input states to the work queue for database or remote resolution.
nextRequests.addAll(downloadedTxns.flatMap { it.inputs }.map { it.txhash })
// Add all input states to the work queue.
val inputHashes = downloads.flatMap { it.tx.inputs }.map { it.txhash }
nextRequests.addAll(inputHashes)
// And loop around ...
// TODO: Figure out a more appropriate DOS limit here, 5000 is simply a guess.
// TODO: Figure out a more appropriate DOS limit here, 5000 is simply a very bad guess.
// TODO: Unit test the DoS limit.
limitCounter += nextRequests.size
limitCounter = limitCounter checkedAdd nextRequests.size
if (limitCounter > 5000)
throw ExcessivelyLargeTransactionGraph()
}
return resultQ.values.reversed()
}
/**
* Returns a list of all the dependencies of the given transactions, deepest first i.e. the last downloaded comes
* first in the returned list and thus doesn't have any unverified dependencies.
*/
@Suspendable
private fun resolveMissingAttachments(downloads: List<SignedTransaction>) {
private fun fetchMissingAttachments(downloads: List<WireTransaction>) {
// TODO: This could be done in parallel with other fetches for extra speed.
val missingAttachments = downloads.flatMap { stx ->
stx.tx.attachments.filter { serviceHub.storageService.attachments.openAttachment(it) == null }
stx.attachments.filter { serviceHub.storageService.attachments.openAttachment(it) == null }
}
subProtocol(FetchAttachmentsProtocol(missingAttachments.toSet(), otherSide))
}

View File

@ -98,21 +98,21 @@ object TwoPartyDealProtocol {
fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData<SignedTransaction>): SignedTransaction {
progressTracker.currentStep = VERIFYING
untrustedPartialTX.validate {
untrustedPartialTX.validate { stx ->
progressTracker.nextStep()
// Check that the tx proposed by the buyer is valid.
val missingSigs = it.verify(throwIfSignaturesAreMissing = false)
val missingSigs = stx.verifySignatures(throwIfSignaturesAreMissing = false)
if (missingSigs != setOf(myKeyPair.public, notaryNode.identity.owningKey))
throw SignatureException("The set of missing signatures is not as expected: $missingSigs")
val wtx: WireTransaction = it.tx
logger.trace { "Received partially signed transaction: ${it.id}" }
val wtx: WireTransaction = stx.tx
logger.trace { "Received partially signed transaction: ${stx.id}" }
checkDependencies(it)
checkDependencies(stx)
// This verifies that the transaction is contract-valid, even though it is missing signatures.
serviceHub.verifyTransaction(wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments))
wtx.toLedgerTransaction(serviceHub).verify()
// There are all sorts of funny games a malicious secondary might play here, we should fix them:
//
@ -124,7 +124,7 @@ object TwoPartyDealProtocol {
// but the goal of this code is not to be fully secure (yet), but rather, just to find good ways to
// express protocol state machines on top of the messaging layer.
return it
return stx
}
}
@ -226,7 +226,7 @@ object TwoPartyDealProtocol {
logger.trace { "Got signatures from other party, verifying ... " }
val fullySigned = stx + signatures.sellerSig + signatures.notarySig
fullySigned.verify()
fullySigned.verifySignatures()
logger.trace { "Signatures received are valid. Deal transaction complete! :-)" }

View File

@ -23,11 +23,11 @@ class ValidatingNotaryProtocol(otherSide: Party,
uniquenessProvider: UniquenessProvider) : NotaryProtocol.Service(otherSide, sessionIdForSend, sessionIdForReceive, timestampChecker, uniquenessProvider) {
@Suspendable
override fun beforeCommit(stx: SignedTransaction, reqIdentity: Party) {
val wtx = stx.tx
try {
checkSignatures(stx)
validateDependencies(reqIdentity, wtx)
checkContractValid(wtx)
val wtx = stx.tx
resolveTransaction(reqIdentity, wtx)
wtx.toLedgerTransaction(serviceHub).verify()
} catch (e: Exception) {
when (e) {
is TransactionVerificationException,
@ -39,18 +39,13 @@ class ValidatingNotaryProtocol(otherSide: Party,
private fun checkSignatures(stx: SignedTransaction) {
val myKey = serviceHub.storageService.myLegalIdentity.owningKey
val missing = stx.verify(false) - myKey
val missing = stx.verifySignatures(throwIfSignaturesAreMissing = false) - myKey
if (missing.isNotEmpty()) throw NotaryException(NotaryError.SignaturesMissing(missing.toList()))
}
private fun checkContractValid(wtx: WireTransaction) {
val ltx = wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
serviceHub.verifyTransaction(ltx)
}
@Suspendable
private fun validateDependencies(reqIdentity: Party, wtx: WireTransaction) {
private fun resolveTransaction(reqIdentity: Party, wtx: WireTransaction) {
subProtocol(ResolveTransactionsProtocol(wtx, reqIdentity))
}
}

View File

@ -1,194 +0,0 @@
package com.r3corda.core.contracts
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.newSecureRandom
import com.r3corda.core.node.services.testing.MockStorageService
import com.r3corda.core.testing.*
import org.junit.Test
import java.security.PublicKey
import java.util.*
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNotEquals
val TEST_PROGRAM_ID = TransactionGroupTests.TestCash()
class TransactionGroupTests {
val A_THOUSAND_POUNDS = TestCash.State(MINI_CORP.ref(1, 2, 3), 1000.POUNDS, MINI_CORP_PUBKEY)
class TestCash : Contract {
override val legalContractReference = SecureHash.sha256("TestCash")
override fun verify(tx: TransactionForContract) {
}
data class State(
val deposit: PartyAndReference,
val amount: Amount<Currency>,
override val owner: PublicKey) : OwnableState {
override val contract: Contract = TEST_PROGRAM_ID
override val participants: List<PublicKey>
get() = listOf(owner)
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
}
interface Commands : CommandData {
class Move() : TypeOnlyCommandData(), Commands
data class Issue(val nonce: Long = newSecureRandom().nextLong()) : Commands
data class Exit(val amount: Amount<Currency>) : Commands
}
}
infix fun TestCash.State.`owned by`(owner: PublicKey) = copy(owner = owner)
infix fun TestCash.State.`with notary`(notary: Party) = TransactionState(this, notary)
@Test
fun success() {
ledger {
unverifiedTransaction {
output("£1000") { A_THOUSAND_POUNDS }
}
transaction {
input("£1000")
output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE_PUBKEY }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Move() }
this.verifies()
}
transaction {
input("alice's £1000")
command(ALICE_PUBKEY) { TestCash.Commands.Move() }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Exit(1000.POUNDS) }
this.verifies()
}
this.verifies()
}
}
@Test
fun conflict() {
ledger {
val t = transaction {
output("cash") { A_THOUSAND_POUNDS }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() }
this.verifies()
}
val conflict1 = transaction {
input("cash")
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` BOB_PUBKEY
output { HALF }
output { HALF }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Move() }
this.verifies()
}
verifies()
// Alice tries to double spend back to herself.
val conflict2 = transaction {
input("cash")
val HALF = A_THOUSAND_POUNDS.copy(amount = 500.POUNDS) `owned by` ALICE_PUBKEY
output { HALF }
output { HALF }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Move() }
this.verifies()
}
assertNotEquals(conflict1, conflict2)
val e = assertFailsWith(TransactionConflictException::class) {
verifies()
}
assertEquals(StateRef(t.id, 0), e.conflictRef)
assertEquals(setOf(conflict1.id, conflict2.id), setOf(e.tx1.id, e.tx2.id))
}
}
@Test
fun disconnected() {
// Check that if we have a transaction in the group that doesn't connect to anything else, it's rejected.
val tg = ledger {
transaction {
output("cash") { A_THOUSAND_POUNDS }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() }
this.verifies()
}
transaction {
input("cash")
output { A_THOUSAND_POUNDS `owned by` BOB_PUBKEY }
this.verifies()
}
}
val input = StateAndRef(A_THOUSAND_POUNDS `with notary` DUMMY_NOTARY, generateStateRef())
tg.apply {
transaction {
assertFailsWith(TransactionResolutionException::class) {
input(input.ref)
}
this.verifies()
}
}
}
@Test
fun duplicatedInputs() {
// Check that a transaction cannot refer to the same input more than once.
ledger {
unverifiedTransaction {
output("£1000") { A_THOUSAND_POUNDS }
}
transaction {
input("£1000")
input("£1000")
output { A_THOUSAND_POUNDS.copy(amount = A_THOUSAND_POUNDS.amount * 2) }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Move() }
this.verifies()
}
assertFailsWith(TransactionConflictException::class) {
verifies()
}
}
}
@Test
fun signGroup() {
ledger {
transaction {
output("£1000") { A_THOUSAND_POUNDS }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Issue() }
this.verifies()
}
transaction {
input("£1000")
output("alice's £1000") { A_THOUSAND_POUNDS `owned by` ALICE_PUBKEY }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Move() }
this.verifies()
}
transaction {
input("alice's £1000")
command(ALICE_PUBKEY) { TestCash.Commands.Move() }
command(MINI_CORP_PUBKEY) { TestCash.Commands.Exit(1000.POUNDS) }
this.verifies()
}
val signedTxns: List<SignedTransaction> = signAll()
// Now go through the conversion -> verification path with them.
val ltxns = signedTxns.map {
it.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments)
}.toSet()
TransactionGroup(ltxns, emptySet()).verify()
}
}
}

View File

@ -3,7 +3,6 @@ package com.r3corda.core.serialization
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.newSecureRandom
import com.r3corda.core.node.services.testing.MockStorageService
import com.r3corda.core.seconds
import com.r3corda.core.testing.*
import org.junit.Before
@ -97,7 +96,7 @@ class TransactionSerializationTests {
tx2.signWith(DUMMY_NOTARY_KEY)
tx2.signWith(DUMMY_KEY_2)
signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verify()
signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verifySignatures()
}
}
@ -107,10 +106,6 @@ class TransactionSerializationTests {
tx.signWith(DUMMY_KEY_1)
tx.signWith(DUMMY_NOTARY_KEY)
val stx = tx.toSignedTransaction()
val ltx = stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments)
assertEquals(tx.commands().map { it.value }, ltx.commands.map { it.value })
assertEquals(tx.inputStates(), ltx.inputs)
assertEquals(tx.outputStates(), ltx.outputs)
assertEquals(TEST_TX_TIME, ltx.commands.getTimestampBy(DUMMY_NOTARY)!!.midpoint)
assertEquals(TEST_TX_TIME, (stx.tx.commands[1].value as TimestampCommand).midpoint)
}
}

View File

@ -3,15 +3,14 @@ package com.r3corda.contracts
import com.r3corda.core.contracts.DOLLARS
import com.r3corda.core.contracts.LedgerTransaction
import com.r3corda.core.contracts.`issued by`
import com.r3corda.core.contracts.verifyToLedgerTransaction
import com.r3corda.core.node.services.testing.MockStorageService
import com.r3corda.core.contracts.toLedgerTransaction
import com.r3corda.core.node.services.testing.MockServices
import com.r3corda.core.seconds
import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.testing.*
import org.junit.Before
import java.time.Instant
import java.time.ZoneOffset
//import java.util.*
//import kotlin.test.fail
/**
* unit test cases that confirms the correct behavior of the AccountReceivable smart contract
@ -29,6 +28,8 @@ class AccountReceivableTests {
val notary = DUMMY_NOTARY
lateinit var services: MockServices
val invoiceProperties = Invoice.InvoiceProperties(
invoiceID = "123",
seller = LocDataStructures.Company(
@ -67,6 +68,11 @@ class AccountReceivableTests {
PAST, FUTURE
}
@Before
fun setup() {
services = MockServices()
}
fun generateInvoiceIssueTxn(kind: WhatKind = WhatKind.FUTURE): LedgerTransaction {
val genTX: LedgerTransaction = run {
val pastProp = initialInvoiceState.props.copy(invoiceDate =
@ -83,8 +89,9 @@ class AccountReceivableTests {
signWith(MINI_CORP_KEY)
signWith(DUMMY_NOTARY_KEY)
}
gtx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments)
gtx.toSignedTransaction().toLedgerTransaction(services)
}
genTX.verify()
return genTX
}

View File

@ -2,8 +2,9 @@ package com.r3corda.contracts
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.services.testing.MockStorageService
import com.r3corda.core.node.services.testing.MockServices
import com.r3corda.core.testing.*
import org.junit.Before
import org.junit.Test
import java.time.Instant
import java.time.LocalDate
@ -42,7 +43,12 @@ class BillOfLadingAgreementTests {
props =pros
)
val attachments = MockStorageService().attachments
lateinit var services: MockServices
@Before
fun setup() {
services = MockServices()
}
//Generation method tests
@ -52,15 +58,14 @@ class BillOfLadingAgreementTests {
signWith(ALICE_KEY)
signWith(DUMMY_NOTARY_KEY)
}
val stx = ptx.toSignedTransaction()
stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments)
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
}
@Test(expected = IllegalStateException::class)
fun issueGenerationMethod_Unsigned() {
val ptx = BillOfLadingAgreement().generateIssue(Bill.owner, Bill.beneficiary, Bill.props, DUMMY_NOTARY)
val stx = ptx.toSignedTransaction()
stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments)
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
}
@Test(expected = IllegalStateException::class)
@ -68,9 +73,7 @@ class BillOfLadingAgreementTests {
val ptx = BillOfLadingAgreement().generateIssue(Bill.owner, Bill.beneficiary, Bill.props, DUMMY_NOTARY).apply {
signWith(BOB_KEY)
}
val stx = ptx.toSignedTransaction()
stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments)
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
}
// @Test // TODO: Fix Test
@ -85,8 +88,7 @@ class BillOfLadingAgreementTests {
ptx.signWith(MEGA_CORP_KEY) //Signed by owner
ptx.signWith(BOB_KEY) //and beneficiary
// ptx.signWith(CHARLIE_KEY) // ??????
val stx = ptx.toSignedTransaction()
stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments)
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
}
@Test(expected = IllegalStateException::class)
@ -98,8 +100,7 @@ class BillOfLadingAgreementTests {
)
BillOfLadingAgreement().generateTransferAndEndorse(ptx,sr,CHARLIE_PUBKEY, CHARLIE)
ptx.signWith(MEGA_CORP_KEY) //Signed by owner
val stx = ptx.toSignedTransaction()
stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments)
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
}
@Test(expected = IllegalStateException::class)
@ -111,8 +112,7 @@ class BillOfLadingAgreementTests {
)
BillOfLadingAgreement().generateTransferAndEndorse(ptx,sr,CHARLIE_PUBKEY, CHARLIE)
ptx.signWith(BOB_KEY) //Signed by beneficiary
val stx = ptx.toSignedTransaction()
stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments)
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
}
// @Test // TODO Fix Test
@ -124,8 +124,7 @@ class BillOfLadingAgreementTests {
)
BillOfLadingAgreement().generateTransferPossession(ptx,sr,CHARLIE_PUBKEY)
ptx.signWith(MEGA_CORP_KEY) //Signed by owner
val stx = ptx.toSignedTransaction()
stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments)
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
}
@Test(expected = IllegalStateException::class)
@ -136,8 +135,7 @@ class BillOfLadingAgreementTests {
StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
)
BillOfLadingAgreement().generateTransferPossession(ptx,sr,CHARLIE_PUBKEY)
val stx = ptx.toSignedTransaction()
stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments)
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
}
//Custom transaction tests

View File

@ -3,8 +3,10 @@ package com.r3corda.contracts
import com.r3corda.contracts.asset.Cash
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.services.testing.MockServices
import com.r3corda.core.serialization.OpaqueBytes
import com.r3corda.core.testing.*
import org.junit.Before
import org.junit.Test
import java.time.Instant
import java.time.LocalDate
@ -114,23 +116,26 @@ class LOCTests {
)
)
lateinit var services: MockServices
@Before
fun setup() {
services = MockServices()
}
@Test
fun issueSignedByBank() {
val ptx = LOC().generateIssue(LOCstate.beneficiaryPaid, LOCstate.issued, LOCstate.terminated, LOCstate.props, DUMMY_NOTARY).apply {
val ptx = LOC().generateIssue(LOCstate.beneficiaryPaid, true, LOCstate.terminated, LOCstate.props, DUMMY_NOTARY).apply {
signWith(MEGA_CORP_KEY)
signWith(DUMMY_NOTARY_KEY)
}
val stx = ptx.toSignedTransaction()
stx.verify()
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
}
@Test(expected = IllegalStateException::class)
fun issueUnsigned() {
val ptx = LOC().generateIssue(LOCstate.beneficiaryPaid, LOCstate.issued, LOCstate.terminated, LOCstate.props, DUMMY_NOTARY)
val stx = ptx.toSignedTransaction()
stx.verify()
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
}
@Test(expected = IllegalStateException::class)
@ -138,9 +143,7 @@ class LOCTests {
val ptx = LOC().generateIssue(LOCstate.beneficiaryPaid, LOCstate.issued, LOCstate.terminated, LOCstate.props, DUMMY_NOTARY).apply {
signWith(BOB_KEY)
}
val stx = ptx.toSignedTransaction()
stx.verify()
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
}

View File

@ -257,11 +257,10 @@ class TwoPartyTradeProtocolTests {
}
val attachmentID = attachment(ByteArrayInputStream(stream.toByteArray()))
val issuer = MEGA_CORP.ref(1)
val bobsFakeCash = fillUpForBuyer(false, bobNode.keyManagement.freshKey().public, issuer).second
val bobsFakeCash = fillUpForBuyer(false, bobNode.keyManagement.freshKey().public).second
val bobsSignedTxns = insertFakeTransactions(bobsFakeCash, bobNode.services)
val alicesFakePaper = fillUpForSeller(false, aliceNode.storage.myLegalIdentity.owningKey,
1200.DOLLARS `issued by` issuer, notaryNode.info.identity, attachmentID).second
1200.DOLLARS `issued by` DUMMY_CASH_ISSUER, notaryNode.info.identity, attachmentID).second
val alicesSignedTxns = insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey)
val buyerSessionID = random63BitValue()
@ -326,9 +325,11 @@ class TwoPartyTradeProtocolTests {
TxRecord.Get(bobsFakeCash[0].id),
// Bob answers with the transactions that are now all verifiable, as Alice bottomed out.
// Bob's transactions are valid, so she commits to the database
TxRecord.Add(bobsSignedTxns[bobsFakeCash[1].id]!!),
TxRecord.Add(bobsSignedTxns[bobsFakeCash[2].id]!!),
TxRecord.Add(bobsSignedTxns[bobsFakeCash[0].id]!!),
TxRecord.Get(bobsFakeCash[0].id), // Verify
TxRecord.Add(bobsSignedTxns[bobsFakeCash[2].id]!!),
TxRecord.Get(bobsFakeCash[0].id), // Verify
TxRecord.Add(bobsSignedTxns[bobsFakeCash[1].id]!!),
// Now she verifies the transaction is contract-valid (not signature valid) which means
// looking up the states again.
TxRecord.Get(bobsFakeCash[1].id),
@ -413,7 +414,7 @@ class TwoPartyTradeProtocolTests {
wtxToSign: List<WireTransaction>,
services: ServiceHub,
vararg extraKeys: KeyPair): Map<SecureHash, SignedTransaction> {
val signed: List<SignedTransaction> = signAll(wtxToSign, extraKeys.toList())
val signed: List<SignedTransaction> = signAll(wtxToSign, extraKeys.toList() + DUMMY_CASH_ISSUER_KEY)
services.recordTransactions(signed)
val validatedTransactions = services.storageService.validatedTransactions
if (validatedTransactions is RecordingTransactionStorage) {
@ -425,16 +426,15 @@ class TwoPartyTradeProtocolTests {
private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.fillUpForBuyer(
withError: Boolean,
owner: PublicKey = BOB_PUBKEY,
issuer: PartyAndReference = MEGA_CORP.ref(1)): Pair<Wallet, List<WireTransaction>> {
issuer: PartyAndReference = DUMMY_CASH_ISSUER): Pair<Wallet, List<WireTransaction>> {
// Bob (Buyer) has some cash he got from the Bank of Elbonia, Alice (Seller) has some commercial paper she
// wants to sell to Bob.
val eb1 = transaction {
// Issued money to itself.
output("elbonian money 1") { 800.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY }
output("elbonian money 2") { 1000.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY }
if (!withError)
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
command(DUMMY_CASH_ISSUER_KEY.public) { Cash.Commands.Issue() }
timestamp(TEST_TX_TIME)
if (withError) {
this.fails()

View File

@ -7,7 +7,6 @@ import com.r3corda.contracts.testing.fillWithSomeTestCash
import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.services.WalletService
import com.r3corda.core.node.services.testing.MockStorageService
import com.r3corda.core.node.services.testing.MockServices
import com.r3corda.core.testing.*
import com.r3corda.core.utilities.BriefLogFormatter
@ -70,7 +69,7 @@ class WalletWithCashTest {
Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), freshKey.public, DUMMY_NOTARY)
signWith(MEGA_CORP_KEY)
}.toSignedTransaction()
val myOutput = usefulTX.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments).outRef<Cash.State>(0)
val myOutput = usefulTX.toLedgerTransaction(services).outRef<Cash.State>(0)
// A tx that spends our money.
val spendTX = TransactionType.General.Builder().apply {