Merged in mike-ledgertx-refactoring (pull request #264)

Refactor the core transaction types
This commit is contained in:
Mike Hearn 2016-08-08 18:02:32 +02:00
commit 8c00b5284d
24 changed files with 349 additions and 615 deletions

View File

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

View File

@ -1,11 +1,12 @@
package com.r3corda.contracts package com.r3corda.contracts
import com.r3corda.contracts.asset.* import com.r3corda.contracts.asset.*
import com.r3corda.contracts.testing.fillWithSomeTestCash
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.Party import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.days 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.seconds
import com.r3corda.core.testing.* import com.r3corda.core.testing.*
import org.junit.Test import org.junit.Test
@ -72,7 +73,6 @@ class CommercialPaperTestsGeneric {
@Parameterized.Parameter @Parameterized.Parameter
lateinit var thisTest: ICommercialPaperTestTemplate lateinit var thisTest: ICommercialPaperTestTemplate
val attachments = MockStorageService().attachments
val issuer = MEGA_CORP.ref(123) val issuer = MEGA_CORP.ref(123)
@Test @Test
@ -190,59 +190,62 @@ class CommercialPaperTestsGeneric {
@Test @Test
fun `issue move and then redeem`() { fun `issue move and then redeem`() {
// MiniCorp issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself. val aliceServices = MockServices()
val issueTX: LedgerTransaction = run { val alicesWallet = aliceServices.fillWithSomeTestCash(9000.DOLLARS)
val ptx = CommercialPaper().generateIssue(MINI_CORP.ref(123), 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER,
TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { 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) setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
signWith(MINI_CORP_KEY) signWith(bigCorpServices.key)
signWith(DUMMY_NOTARY_KEY) signWith(DUMMY_NOTARY_KEY)
} }.toSignedTransaction()
val stx = ptx.toSignedTransaction()
stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments)
}
val (alicesWalletTX, alicesWallet) = cashOutputsToWallet( // Alice pays $9000 to BigCorp to own some of their debt.
3000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY, val moveTX: SignedTransaction = run {
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 {
val ptx = TransactionType.General.Builder() val ptx = TransactionType.General.Builder()
Cash().generateSpend(ptx, 9000.DOLLARS, MINI_CORP_PUBKEY, alicesWallet) Cash().generateSpend(ptx, 9000.DOLLARS, bigCorpServices.key.public, alicesWallet.statesOfType<Cash.State>())
CommercialPaper().generateMove(ptx, issueTX.outRef(0), ALICE_PUBKEY) CommercialPaper().generateMove(ptx, issueTX.tx.outRef(0), aliceServices.key.public)
ptx.signWith(MINI_CORP_KEY) ptx.signWith(bigCorpServices.key)
ptx.signWith(ALICE_KEY) ptx.signWith(aliceServices.key)
ptx.signWith(DUMMY_NOTARY_KEY) ptx.signWith(DUMMY_NOTARY_KEY)
ptx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments) ptx.toSignedTransaction()
} }
// Won't be validated. fun makeRedeemTX(time: Instant): SignedTransaction {
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 {
val ptx = TransactionType.General.Builder() val ptx = TransactionType.General.Builder()
ptx.setTime(time, DUMMY_NOTARY, 30.seconds) ptx.setTime(time, DUMMY_NOTARY, 30.seconds)
CommercialPaper().generateRedeem(ptx, moveTX.outRef(1), corpWallet) CommercialPaper().generateRedeem(ptx, moveTX.tx.outRef(1), bigCorpWallet.statesOfType<Cash.State>())
ptx.signWith(ALICE_KEY) ptx.signWith(aliceServices.key)
ptx.signWith(MINI_CORP_KEY) ptx.signWith(bigCorpServices.key)
ptx.signWith(DUMMY_NOTARY_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 tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days)
val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.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) { 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")) 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 package com.r3corda.contracts
import com.r3corda.core.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.seconds
import com.r3corda.core.testing.* import com.r3corda.core.testing.*
import org.junit.Test import org.junit.Test
@ -195,9 +195,6 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State {
} }
class IRSTests { class IRSTests {
val attachments = MockStorageService().attachments
@Test @Test
fun ok() { fun ok() {
trade().verifies() trade().verifies()
@ -211,9 +208,9 @@ class IRSTests {
/** /**
* Generate an IRS txn - we'll need it for a few things. * 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 dummyIRS = createDummyIRS(irsSelect)
val genTX: LedgerTransaction = run { val genTX: SignedTransaction = run {
val gtx = InterestRateSwap().generateAgreement( val gtx = InterestRateSwap().generateAgreement(
fixedLeg = dummyIRS.fixedLeg, fixedLeg = dummyIRS.fixedLeg,
floatingLeg = dummyIRS.floatingLeg, floatingLeg = dummyIRS.floatingLeg,
@ -225,7 +222,7 @@ class IRSTests {
signWith(MINI_CORP_KEY) signWith(MINI_CORP_KEY)
signWith(DUMMY_NOTARY_KEY) signWith(DUMMY_NOTARY_KEY)
} }
gtx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments) gtx.toSignedTransaction()
} }
return genTX return genTX
} }
@ -243,7 +240,7 @@ class IRSTests {
* Utility so I don't have to keep typing this. * Utility so I don't have to keep typing this.
*/ */
fun singleIRS(irsSelector: Int = 1): InterestRateSwap.State { 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 @Test
fun generateIRSandFixSome() { fun generateIRSandFixSome() {
val services = MockServices()
var previousTXN = generateIRSTxn(1) var previousTXN = generateIRSTxn(1)
fun currentIRS() = previousTXN.outputs.map { it.data }.filterIsInstance<InterestRateSwap.State>().single() previousTXN.toLedgerTransaction(services).verify()
services.recordTransactions(previousTXN)
val txns = HashSet<LedgerTransaction>() fun currentIRS() = previousTXN.tx.outputs.map { it.data }.filterIsInstance<InterestRateSwap.State>().single()
txns += previousTXN
while (true) { while (true) {
val nextFix: FixOf = currentIRS().nextFixingOf() ?: break val nextFix: FixOf = currentIRS().nextFixingOf() ?: break
val fixTX: LedgerTransaction = run { val fixTX: SignedTransaction = run {
val tx = TransactionType.General.Builder() val tx = TransactionType.General.Builder()
val fixing = Fix(nextFix, "0.052".percent.value) 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) { with(tx) {
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds) setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
signWith(MEGA_CORP_KEY) signWith(MEGA_CORP_KEY)
signWith(MINI_CORP_KEY) signWith(MINI_CORP_KEY)
signWith(DUMMY_NOTARY_KEY) signWith(DUMMY_NOTARY_KEY)
} }
tx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments) tx.toSignedTransaction()
} }
fixTX.toLedgerTransaction(services).verify()
services.recordTransactions(fixTX)
previousTXN = fixTX previousTXN = fixTX
txns += fixTX
} }
TransactionGroup(txns, emptySet()).verify()
} }
// Move these later as they aren't IRS specific. // Move these later as they aren't IRS specific.

View File

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

View File

@ -1,32 +1,39 @@
package com.r3corda.core.contracts package com.r3corda.core.contracts
import com.r3corda.core.node.services.AttachmentStorage import com.r3corda.core.node.ServiceHub
import com.r3corda.core.node.services.IdentityService
import java.io.FileNotFoundException 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 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, fun WireTransaction.toLedgerTransaction(services: ServiceHub): LedgerTransaction {
attachmentStorage: AttachmentStorage): 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 authenticatedArgs = commands.map {
val institutions = it.signers.mapNotNull { pk -> identityService.partyFromKey(pk) } val parties = it.signers.mapNotNull { pk -> services.identityService.partyFromKey(pk) }
AuthenticatedObject(it.signers, institutions, it.value) AuthenticatedObject(it.signers, parties, it.value)
} }
// Open attachments specified in this transaction. If we haven't downloaded them, we fail.
val attachments = attachments.map { 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 * Calls [verify] to check all required signatures are present, and then calls [WireTransaction.toLedgerTransaction]
* [WireTransaction.toLedgerTransaction] to look up well known identities from pubkeys. * 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, fun SignedTransaction.toLedgerTransaction(services: ServiceHub): LedgerTransaction {
attachmentStorage: AttachmentStorage): LedgerTransaction { verifySignatures()
verify() return tx.toLedgerTransaction(services)
return tx.toLedgerTransaction(identityService, attachmentStorage)
} }

View File

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

View File

@ -8,76 +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. // 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>>,
val attachments: List<Attachment>,
val commands: List<AuthenticatedObject<CommandData>>,
val origHash: SecureHash,
val signers: List<PublicKey>,
val type: TransactionType) {
override fun hashCode() = origHash.hashCode()
override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash
/**
* 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())
}
/** /**
* A transaction to be passed as input to a contract verification function. Defines helper methods to * A transaction to be passed as input to a contract verification function. Defines helper methods to
* simplify verification logic in contracts. * simplify verification logic in contracts.
@ -171,14 +101,16 @@ data class TransactionForContract(val inputs: List<ContractState>,
fun getTimestampByName(vararg authorityName: String): TimestampCommand? = commands.getTimestampByName(*authorityName) 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() class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception()
sealed class TransactionVerificationException(val tx: TransactionForVerification, cause: Throwable?) : Exception(cause) { sealed class TransactionVerificationException(val tx: LedgerTransaction, cause: Throwable?) : Exception(cause) {
class ContractRejection(tx: TransactionForVerification, val contract: Contract, cause: Throwable?) : TransactionVerificationException(tx, cause) class ContractRejection(tx: LedgerTransaction, val contract: Contract, cause: Throwable?) : TransactionVerificationException(tx, cause)
class MoreThanOneNotary(tx: TransactionForVerification) : TransactionVerificationException(tx, null) class MoreThanOneNotary(tx: LedgerTransaction) : TransactionVerificationException(tx, null)
class SignersMissing(tx: TransactionForVerification, val missing: List<PublicKey>) : TransactionVerificationException(tx, null) { class SignersMissing(tx: LedgerTransaction, val missing: List<PublicKey>) : TransactionVerificationException(tx, null) {
override fun toString() = "Signers missing: ${missing.map { it.toStringShort() }}" 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

@ -20,24 +20,27 @@ import java.security.SignatureException
* SignedTransaction wraps a serialized WireTransaction. It contains one or more signatures, each one for * SignedTransaction wraps a serialized WireTransaction. It contains one or more signatures, each one for
* a public key that is mentioned inside a transaction command. SignedTransaction is the top level transaction type * 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 * 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 * of a WireTransaction, therefore if you are storing data keyed by WT hash be aware that multiple different STs 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 * 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 * 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 * keypairs.
* WireTransaction i.e. the outermost serialised form with everything included.
* *
* LedgerTransaction is derived from WireTransaction. It is the result of doing some basic key lookups on WireCommand * LedgerTransaction is derived from WireTransaction. It is the result of doing the following operations:
* 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 * - Downloading and locally storing all the dependencies of the transaction.
* sense to use a certificate scheme and so that logic would get more complex. * - Resolving the input states and loading them into memory.
* - 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 output states.
* *
* All the above refer to inputs using a (txhash, output index) pair. * All the above refer to inputs using a (txhash, output index) pair.
* *
* TransactionForVerification is the same as LedgerTransaction but with the input states looked up from a local * There is also TransactionForContract, which is a lightly red-acted form of LedgerTransaction that's fed into the
* database and replaced with the real objects. Likewise, attachments are fully resolved at this point. * contract's verify function. It may be removed in future.
* TFV is the form that is finally fed into the contracts.
*/ */
/** Transaction ready for serialisation, without any signatures attached. */ /** Transaction ready for serialisation, without any signatures attached. */
@ -73,7 +76,7 @@ data class WireTransaction(val inputs: List<StateRef>,
override fun toString(): String { override fun toString(): String {
val buf = StringBuilder() val buf = StringBuilder()
buf.appendln("Transaction:") buf.appendln("Transaction $id:")
for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input") for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input")
for (output in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $output") for (output in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $output")
for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command") for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command")
@ -97,18 +100,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. */ /** 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 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 * 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 * the set of pubkeys in the signers list. If any signatures are missing, either throws an exception (by default) or
@ -116,9 +107,12 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
* *
* @throws SignatureException if a signature is invalid, does not match or if any signature is missing. * @throws SignatureException if a signature is invalid, does not match or if any signature is missing.
*/ */
fun verify(throwIfSignaturesAreMissing: Boolean = true): Set<PublicKey> { fun verifySignatures(throwIfSignaturesAreMissing: Boolean = true): Set<PublicKey> {
verifySignatures() // 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() val missing = getMissingSignatures()
if (missing.isNotEmpty() && throwIfSignaturesAreMissing) if (missing.isNotEmpty() && throwIfSignaturesAreMissing)
throw SignatureException("Missing signatures on transaction ${id.prefixChars()} for: ${missing.map { it.toStringShort() }}") throw SignatureException("Missing signatures on transaction ${id.prefixChars()} for: ${missing.map { it.toStringShort() }}")
@ -144,7 +138,7 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
/** /**
* Returns the set of missing signatures - a signature must be present for each signer public key. * Returns the set of missing signatures - a signature must be present for each signer public key.
*/ */
fun getMissingSignatures(): Set<PublicKey> { private fun getMissingSignatures(): Set<PublicKey> {
val requiredKeys = tx.signers.toSet() val requiredKeys = tx.signers.toSet()
val sigKeys = sigs.map { it.by }.toSet() val sigKeys = sigs.map { it.by }.toSet()
@ -157,12 +151,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. * 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 * 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. * 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( data class LedgerTransaction(
/** The input states which will be consumed/invalidated by the execution of this transaction. */ /** 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. */ /** The states that will be generated by the execution of this transaction. */
val outputs: List<TransactionState<*>>, val outputs: List<TransactionState<*>>,
/** Arbitrary data passed to the program of each input state. */ /** Arbitrary data passed to the program of each input state. */
@ -171,9 +163,28 @@ data class LedgerTransaction(
val attachments: List<Attachment>, val attachments: List<Attachment>,
/** The hash of the original serialised WireTransaction */ /** The hash of the original serialised WireTransaction */
override val id: SecureHash, 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 signers: List<PublicKey>,
val type: TransactionType val type: TransactionType
) : NamedByHash { ) : NamedByHash {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : ContractState> outRef(index: Int) = StateAndRef(outputs[index] as TransactionState<T>, StateRef(id, index)) 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 package com.r3corda.core.node
import com.google.common.util.concurrent.ListenableFuture 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.messaging.MessagingService
import com.r3corda.core.node.services.* import com.r3corda.core.node.services.*
import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.protocols.ProtocolLogic
@ -25,19 +28,6 @@ interface ServiceHub {
val schedulerService: SchedulerService val schedulerService: SchedulerService
val clock: Clock 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 * 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. * sends them to the wallet for further processing.
@ -70,4 +60,4 @@ interface ServiceHub {
* @throws IllegalProtocolLogicException or IllegalArgumentException if there are problems with the [logicType] or [args]. * @throws IllegalProtocolLogicException or IllegalArgumentException if there are problems with the [logicType] or [args].
*/ */
fun <T : Any> invokeProtocolAsync(logicType: Class<out ProtocolLogic<T>>, vararg args: Any?): ListenableFuture<T> fun <T : Any> invokeProtocolAsync(logicType: Class<out ProtocolLogic<T>>, vararg args: Any?): ListenableFuture<T>
} }

View File

@ -1,10 +1,7 @@
package com.r3corda.core.testing package com.r3corda.core.testing
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.DigitalSignature import com.r3corda.core.crypto.*
import com.r3corda.core.crypto.Party
import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.signWithECDSA
import com.r3corda.core.node.ServiceHub import com.r3corda.core.node.ServiceHub
import com.r3corda.core.serialization.serialize import com.r3corda.core.serialization.serialize
import java.io.InputStream import java.io.InputStream
@ -133,8 +130,7 @@ data class TestTransactionDSLInterpreter private constructor(
} }
override fun verifies(): EnforceVerifyOrFail { override fun verifies(): EnforceVerifyOrFail {
val resolvedTransaction = ledgerInterpreter.resolveWireTransaction(toWireTransaction()) toWireTransaction().toLedgerTransaction(services).verify()
resolvedTransaction.verify()
return EnforceVerifyOrFail.Token return EnforceVerifyOrFail.Token
} }
@ -185,26 +181,6 @@ data class TestLedgerDSLInterpreter private constructor (
nonVerifiedTransactionWithLocations = HashMap(nonVerifiedTransactionWithLocations) 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> { internal inline fun <reified S : ContractState> resolveStateRef(stateRef: StateRef): TransactionState<S> {
val transactionWithLocation = val transactionWithLocation =
transactionWithLocations[stateRef.txhash] ?: transactionWithLocations[stateRef.txhash] ?:
@ -230,16 +206,6 @@ data class TestLedgerDSLInterpreter private constructor (
return transactionInterpreter 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? { fun transactionName(transactionHash: SecureHash): String? {
val transactionWithLocation = transactionWithLocations[transactionHash] val transactionWithLocation = transactionWithLocations[transactionHash]
return if (transactionWithLocation != null) { return if (transactionWithLocation != null) {
@ -298,16 +264,18 @@ data class TestLedgerDSLInterpreter private constructor (
} }
override fun verifies(): EnforceVerifyOrFail { override fun verifies(): EnforceVerifyOrFail {
val transactionGroup = toTransactionGroup()
try { 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) { } catch (exception: TransactionVerificationException) {
val transactionWithLocation = transactionWithLocations[exception.tx.origHash] val transactionWithLocation = transactionWithLocations[exception.tx.id]
val transactionName = transactionWithLocation?.label ?: transactionWithLocation?.location ?: "<unknown>" val transactionName = transactionWithLocation?.label ?: transactionWithLocation?.location ?: "<unknown>"
throw VerifiesFailed(transactionName, exception) throw VerifiesFailed(transactionName, exception)
} }
return EnforceVerifyOrFail.Token
} }
override fun <S : ContractState> retrieveOutputStateAndRef(clazz: Class<S>, label: String): StateAndRef<S> { 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.protocols.ProtocolLogic
import com.r3corda.core.random63BitValue import com.r3corda.core.random63BitValue
import com.r3corda.core.utilities.ProgressTracker import com.r3corda.core.utilities.ProgressTracker
import com.r3corda.protocols.NotaryProtocol import com.r3corda.protocols.AbstractStateReplacementProtocol.Acceptor
import com.r3corda.protocols.PartyRequestMessage import com.r3corda.protocols.AbstractStateReplacementProtocol.Instigator
import com.r3corda.protocols.ResolveTransactionsProtocol
import java.security.PublicKey import java.security.PublicKey
/** /**
@ -164,13 +163,14 @@ abstract class AbstractStateReplacementProtocol<T> {
val response = Result.noError(mySignature) val response = Result.noError(mySignature)
val swapSignatures = sendAndReceive<List<DigitalSignature.WithKey>>(otherSide, sessionIdForSend, sessionIdForReceive, response) 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 -> val allSignatures = swapSignatures.validate { signatures ->
signatures.forEach { it.verifyWithECDSA(stx.txBits) } signatures.forEach { it.verifyWithECDSA(stx.txBits) }
signatures signatures
} }
val finalTx = stx + allSignatures val finalTx = stx + allSignatures
finalTx.verify() finalTx.verifySignatures()
serviceHub.recordTransactions(listOf(finalTx)) serviceHub.recordTransactions(listOf(finalTx))
} }
@ -191,7 +191,9 @@ abstract class AbstractStateReplacementProtocol<T> {
private fun verifyTx(stx: SignedTransaction) { private fun verifyTx(stx: SignedTransaction) {
checkMySignatureRequired(stx.tx) checkMySignatureRequired(stx.tx)
checkDependenciesValid(stx) 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) { private fun checkMySignatureRequired(tx: WireTransaction) {
@ -202,19 +204,10 @@ abstract class AbstractStateReplacementProtocol<T> {
@Suspendable @Suspendable
private fun checkDependenciesValid(stx: SignedTransaction) { private fun checkDependenciesValid(stx: SignedTransaction) {
val dependencyTxIDs = stx.tx.inputs.map { it.txhash }.toSet() subProtocol(ResolveTransactionsProtocol(stx.tx, otherSide))
subProtocol(ResolveTransactionsProtocol(dependencyTxIDs, otherSide))
} }
private fun checkValid(stx: SignedTransaction) { private fun sign(stx: SignedTransaction) = serviceHub.storageService.myLegalIdentityKey.signWithECDSA(stx.txBits)
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)
}
} }
// TODO: similar classes occur in other places (NotaryProtocol), need to consolidate // TODO: similar classes occur in other places (NotaryProtocol), need to consolidate

View File

@ -1,27 +1,35 @@
package com.r3corda.protocols package com.r3corda.protocols
import co.paralleluniverse.fibers.Suspendable 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.Party
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.protocols.ProtocolLogic import com.r3corda.core.protocols.ProtocolLogic
import java.util.* 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 * This protocol is used to verify the validity of a transaction by recursively checking the validity of all the
* their dependencies, and verifies them together using a single [TransactionGroup]. If no exception is thrown, then * dependencies. Once a transaction is checked it's inserted into local storage so it can be relayed and won't be
* all the transactions have been successfully verified and inserted into the local database. * checked again.
* *
* A couple of constructors are provided that accept a single transaction. When these are used, the dependencies of that * 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 * 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 * 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 * then this isn't enough to put into the local database, so only the dependencies are checked and inserted. This way
* protocol is helpful when resolving and verifying a finished but partially signed transaction. * 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>, class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
private val otherSide: Party) : ProtocolLogic<Unit>() { private val otherSide: Party) : ProtocolLogic<List<LedgerTransaction>>() {
companion object { companion object {
private fun dependencyIDs(wtx: WireTransaction) = wtx.inputs.map { it.txhash }.toSet() private fun dependencyIDs(wtx: WireTransaction) = wtx.inputs.map { it.txhash }.toSet()
@ -48,45 +56,46 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
} }
@Suspendable @Suspendable
override fun call(): Unit { override fun call(): List<LedgerTransaction> {
val toVerify = HashSet<LedgerTransaction>() val newTxns: Iterable<SignedTransaction> = downloadDependencies(txHashes)
val alreadyVerified = HashSet<LedgerTransaction>()
val downloadedSignedTxns = ArrayList<SignedTransaction>()
// This fills out toVerify, alreadyVerified (roots) and downloadedSignedTxns. // For each transaction, verify it and insert it into the database. As we are iterating over them in a
fetchDependenciesAndCheckSignatures(txHashes, toVerify, alreadyVerified, downloadedSignedTxns) // 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) { for (tx in newTxns) {
// Check the signatures on the stx first. // Resolve to a LedgerTransaction and then run all contracts.
toVerify += stx!!.verifyToLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments) val ltx = tx.toLedgerTransaction(serviceHub)
} else if (wtx != null) { ltx.verify()
wtx!!.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments) serviceHub.recordTransactions(tx)
result += ltx
} }
// Run all the contracts and throw an exception if any of them reject. // If this protocol is resolving a specific transaction, make sure we have its attachments and then verify
TransactionGroup(toVerify, alreadyVerified).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
// Now write all the transactions we just validated back to the database for next time, including // be a clearer API if we do that. But for consistency with the other c'tor we currently do not.
// 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 // If 'stx' is set, then 'wtx' is the contents (from the c'tor).
// here at the end. Doing it this way avoids cases where a transaction is in the database but its stx?.verifySignatures()
// dependencies aren't, or an unvalidated and possibly broken tx is there. wtx?.let {
serviceHub.recordTransactions(downloadedSignedTxns) fetchMissingAttachments(listOf(it))
val ltx = it.toLedgerTransaction(serviceHub)
ltx.verify()
result += ltx
}
return result
} }
override val topic: String get() = throw UnsupportedOperationException() override val topic: String get() = throw UnsupportedOperationException()
@Suspendable @Suspendable
private fun fetchDependenciesAndCheckSignatures(depsToCheck: Set<SecureHash>, private fun downloadDependencies(depsToCheck: Set<SecureHash>): List<SignedTransaction> {
toVerify: HashSet<LedgerTransaction>, // Maintain a work queue of all hashes to load/download, initialised with our starting set. Then do a breadth
alreadyVerified: HashSet<LedgerTransaction>, // first traversal across the dependency graph.
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.
// //
// TODO: This approach has two problems. Analyze and resolve them: // TODO: This approach has two problems. Analyze and resolve them:
// //
@ -103,45 +112,49 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
val nextRequests = LinkedHashSet<SecureHash>() // Keep things unique but ordered, for unit test stability. val nextRequests = LinkedHashSet<SecureHash>() // Keep things unique but ordered, for unit test stability.
nextRequests.addAll(depsToCheck) nextRequests.addAll(depsToCheck)
val resultQ = LinkedHashMap<SecureHash, SignedTransaction>()
var limitCounter = 0 var limitCounter = 0
while (nextRequests.isNotEmpty()) { 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() nextRequests.clear()
// TODO: This could be done in parallel with other fetches for extra speed. if (notAlreadyFetched.isEmpty()) // Done early.
resolveMissingAttachments(downloads) break
// Resolve any legal identities from known public keys in the signatures. // Request the standalone transaction data (which may refer to things we don't yet have).
val downloadedTxns = downloads.map { val downloads: List<SignedTransaction> = subProtocol(FetchTransactionsProtocol(notAlreadyFetched, otherSide)).downloaded
it.verifyToLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
}
// Do the same for transactions loaded from disk (i.e. we checked them previously). fetchMissingAttachments(downloads.map { it.tx })
val loadedTxns = fromDisk.map {
it.verifyToLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
}
toVerify.addAll(downloadedTxns) for (stx in downloads)
alreadyVerified.addAll(loadedTxns) check(resultQ.putIfAbsent(stx.id, stx) == null) // Assert checks the filter at the start.
downloadedSignedTxns.addAll(downloads)
// And now add all the input states to the work queue for database or remote resolution. // Add all input states to the work queue.
nextRequests.addAll(downloadedTxns.flatMap { it.inputs }.map { it.txhash }) 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 very bad guess.
// TODO: Figure out a more appropriate DOS limit here, 5000 is simply a guess.
// TODO: Unit test the DoS limit. // TODO: Unit test the DoS limit.
limitCounter += nextRequests.size limitCounter = limitCounter checkedAdd nextRequests.size
if (limitCounter > 5000) if (limitCounter > 5000)
throw ExcessivelyLargeTransactionGraph() 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 @Suspendable
private fun resolveMissingAttachments(downloads: List<SignedTransaction>) { private fun fetchMissingAttachments(downloads: List<WireTransaction>) {
val missingAttachments = downloads.flatMap { stx -> // TODO: This could be done in parallel with other fetches for extra speed.
stx.tx.attachments.filter { serviceHub.storageService.attachments.openAttachment(it) == null } val missingAttachments = downloads.flatMap { wtx ->
wtx.attachments.filter { serviceHub.storageService.attachments.openAttachment(it) == null }
} }
subProtocol(FetchAttachmentsProtocol(missingAttachments.toSet(), otherSide)) subProtocol(FetchAttachmentsProtocol(missingAttachments.toSet(), otherSide))
} }

View File

@ -98,21 +98,21 @@ object TwoPartyDealProtocol {
fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData<SignedTransaction>): SignedTransaction { fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData<SignedTransaction>): SignedTransaction {
progressTracker.currentStep = VERIFYING progressTracker.currentStep = VERIFYING
untrustedPartialTX.validate { untrustedPartialTX.validate { stx ->
progressTracker.nextStep() progressTracker.nextStep()
// Check that the tx proposed by the buyer is valid. // 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)) if (missingSigs != setOf(myKeyPair.public, notaryNode.identity.owningKey))
throw SignatureException("The set of missing signatures is not as expected: $missingSigs") throw SignatureException("The set of missing signatures is not as expected: $missingSigs")
val wtx: WireTransaction = it.tx val wtx: WireTransaction = stx.tx
logger.trace { "Received partially signed transaction: ${it.id}" } 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. // 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: // 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 // 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. // 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 ... " } logger.trace { "Got signatures from other party, verifying ... " }
val fullySigned = stx + signatures.sellerSig + signatures.notarySig val fullySigned = stx + signatures.sellerSig + signatures.notarySig
fullySigned.verify() fullySigned.verifySignatures()
logger.trace { "Signatures received are valid. Deal transaction complete! :-)" } logger.trace { "Signatures received are valid. Deal transaction complete! :-)" }
@ -471,4 +471,4 @@ object TwoPartyDealProtocol {
} }
} }
} }

View File

@ -23,11 +23,11 @@ class ValidatingNotaryProtocol(otherSide: Party,
uniquenessProvider: UniquenessProvider) : NotaryProtocol.Service(otherSide, sessionIdForSend, sessionIdForReceive, timestampChecker, uniquenessProvider) { uniquenessProvider: UniquenessProvider) : NotaryProtocol.Service(otherSide, sessionIdForSend, sessionIdForReceive, timestampChecker, uniquenessProvider) {
@Suspendable @Suspendable
override fun beforeCommit(stx: SignedTransaction, reqIdentity: Party) { override fun beforeCommit(stx: SignedTransaction, reqIdentity: Party) {
val wtx = stx.tx
try { try {
checkSignatures(stx) checkSignatures(stx)
validateDependencies(reqIdentity, wtx) val wtx = stx.tx
checkContractValid(wtx) resolveTransaction(reqIdentity, wtx)
wtx.toLedgerTransaction(serviceHub).verify()
} catch (e: Exception) { } catch (e: Exception) {
when (e) { when (e) {
is TransactionVerificationException, is TransactionVerificationException,
@ -39,18 +39,13 @@ class ValidatingNotaryProtocol(otherSide: Party,
private fun checkSignatures(stx: SignedTransaction) { private fun checkSignatures(stx: SignedTransaction) {
val myKey = serviceHub.storageService.myLegalIdentity.owningKey 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())) 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 @Suspendable
private fun validateDependencies(reqIdentity: Party, wtx: WireTransaction) { private fun resolveTransaction(reqIdentity: Party, wtx: WireTransaction) {
subProtocol(ResolveTransactionsProtocol(wtx, reqIdentity)) 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.contracts.*
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.crypto.newSecureRandom import com.r3corda.core.crypto.newSecureRandom
import com.r3corda.core.node.services.testing.MockStorageService
import com.r3corda.core.seconds import com.r3corda.core.seconds
import com.r3corda.core.testing.* import com.r3corda.core.testing.*
import org.junit.Before import org.junit.Before
@ -97,7 +96,7 @@ class TransactionSerializationTests {
tx2.signWith(DUMMY_NOTARY_KEY) tx2.signWith(DUMMY_NOTARY_KEY)
tx2.signWith(DUMMY_KEY_2) 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_KEY_1)
tx.signWith(DUMMY_NOTARY_KEY) tx.signWith(DUMMY_NOTARY_KEY)
val stx = tx.toSignedTransaction() val stx = tx.toSignedTransaction()
val ltx = stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments) assertEquals(TEST_TX_TIME, (stx.tx.commands[1].value as TimestampCommand).midpoint)
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)
} }
} }

View File

@ -23,8 +23,8 @@ States contain arbitrary data, but they always contain at minimum a hash of the
Contract code (or just "contracts" in the rest of this document) are globally shared pieces of business logic. Contract code (or just "contracts" in the rest of this document) are globally shared pieces of business logic.
Contracts define a **verify function**, which is a pure function given the entire transaction as input. To be considered Contracts define a **verify function**, which is a pure function given the entire transaction as input. To be considered
valid, the transaction must be **accepted** by the verify function of every contract pointed to by the valid, the transaction must be **accepted** by the verify function of every contract pointed to by the input and output
input and output states. states.
Beyond inputs and outputs, transactions may also contain **commands**, small data packets that Beyond inputs and outputs, transactions may also contain **commands**, small data packets that
the platform does not interpret itself, but which can parameterise execution of the contracts. They can be thought of as the platform does not interpret itself, but which can parameterise execution of the contracts. They can be thought of as

View File

@ -77,6 +77,31 @@ in place of the attachments themselves (see also :doc:`data-model`). Once signed
resolving the attachment references to the attachments. Commands with valid signatures are encapsulated in the resolving the attachment references to the attachments. Commands with valid signatures are encapsulated in the
``AuthenticatedObject`` type. ``AuthenticatedObject`` type.
When constructing a new transaction from scratch, you use ``TransactionBuilder``, which is a mutable transaction that
can be signed once modification of the internals is complete. It is typical for contract classes to expose helper
methods that can contribute to a ``TransactionBuilder``.
Here's an example of building a transaction that creates an issuance of bananas (note that bananas are not a real
contract type in the library):
.. container:: codeset
.. sourcecode:: kotlin
val notaryToUse: Party = ...
val txb = TransactionBuilder(notary = notaryToUse).withItems(BananaState(Amount(20, Bananas), fromCountry = "Elbonia"))
txb.signWith(myKey)
txb.setTime(Instant.now(), notaryToUse, 30.seconds)
// We must disable the check for sufficient signatures, because this transaction is not yet notarised.
val stx = txb.toSignedTransaction(checkSufficientSignatures = false)
// Alternatively, let's just check it verifies pretending it was fully signed. To do this, we get
// a WireTransaction, which is what the SignedTransaction wraps. Thus by verifying that directly we
// skip signature checking.
txb.toWireTransaction().toLedgerTransaction(services).verify()
In a unit test, you would typically use a freshly created ``MockServices`` object, or more realistically, you would
write your tests using the :doc:`domain specific language for writing tests <tutorial-test-dsl>`.
Party and PublicKey Party and PublicKey
------------------- -------------------

View File

@ -43,7 +43,7 @@ We start with the empty ledger:
} }
The DSL keyword ``ledger`` takes a closure that can build up several transactions and may verify their overall The DSL keyword ``ledger`` takes a closure that can build up several transactions and may verify their overall
correctness. correctness. A ledger is effectively a fresh world with no pre-existing transactions or services within it.
Let's add a Cash transaction: Let's add a Cash transaction:
@ -54,7 +54,7 @@ Let's add a Cash transaction:
@Test @Test
fun simpleCashDoesntCompile() { fun simpleCashDoesntCompile() {
val inState = Cash.State( val inState = Cash.State(
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), amount = 1000.DOLLARS `issued by` DUMMY_CASH_ISSUER,
owner = DUMMY_PUBKEY_1 owner = DUMMY_PUBKEY_1
) )
ledger { ledger {
@ -69,7 +69,7 @@ Let's add a Cash transaction:
@Test @Test
public void simpleCashDoesntCompile() { public void simpleCashDoesntCompile() {
Cash.State inState = new Cash.State( Cash.State inState = new Cash.State(
issuedBy(DOLLARS(1000), getMEGA_CORP().ref((byte)1, (byte)1)), issuedBy(DOLLARS(1000), getDUMMY_CASH_ISSUER()),
getDUMMY_PUBKEY_1() getDUMMY_PUBKEY_1()
); );
ledger(l -> { ledger(l -> {
@ -139,7 +139,10 @@ last line of ``transaction``:
The code finally compiles. When run, it produces the following error:: The code finally compiles. When run, it produces the following error::
com.r3corda.core.contracts.TransactionVerificationException$ContractRejection: java.lang.IllegalArgumentException: Failed requirement: for deposit [0101] at issuer MegaCorp the amounts balance com.r3corda.core.contracts.TransactionVerificationException$ContractRejection: java.lang.IllegalArgumentException: Failed requirement: for deposit [01] at issuer Snake Oil Issuer the amounts balance
.. note:: The reference here to the 'Snake Oil Issuer' is because we are using the pre-canned ``DUMMY_CASH_ISSUER``
identity as the issuer of our cash.
The transaction verification failed, because the sum of inputs does not equal the sum of outputs. We can specify that The transaction verification failed, because the sum of inputs does not equal the sum of outputs. We can specify that
this is intended behaviour by changing ``this.verifies()`` to ``this `fails with` "the amounts balance"``: this is intended behaviour by changing ``this.verifies()`` to ``this `fails with` "the amounts balance"``:

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,6 @@ import com.r3corda.contracts.testing.fillWithSomeTestCash
import com.r3corda.core.contracts.* import com.r3corda.core.contracts.*
import com.r3corda.core.crypto.SecureHash import com.r3corda.core.crypto.SecureHash
import com.r3corda.core.node.services.WalletService 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.node.services.testing.MockServices
import com.r3corda.core.testing.* import com.r3corda.core.testing.*
import com.r3corda.core.utilities.BriefLogFormatter 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) Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), freshKey.public, DUMMY_NOTARY)
signWith(MEGA_CORP_KEY) signWith(MEGA_CORP_KEY)
}.toSignedTransaction() }.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. // A tx that spends our money.
val spendTX = TransactionType.General.Builder().apply { val spendTX = TransactionType.General.Builder().apply {