mirror of
https://github.com/corda/corda.git
synced 2024-12-18 20:47:57 +00:00
Refactor the core transaction types to improve clarity, simplify verification and prepare for sandboxing.
Changes include: - LedgerTransaction is now much more central: it represents a fully resolved and looked-up tx, with the inputs available. - TransactionGroup and TransactionForVerification are gone. There is a temporary TransactionForContract class for backwards compatibility but it will also be gone soon. - ResolveTransactionsProtocol is simplified, and now commits a tx to the database as soon as it's determined to be valid. - ServiceHub is now passed in more consistently to verification code, so we can use more services in future more easily e.g. a sandboxing service. - A variety of APIs have been tweaked or documented better.
This commit is contained in:
parent
918de94a22
commit
701fc853ad
@ -4,9 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.contracts.asset.sumCashBy
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.DigitalSignature
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.signWithECDSA
|
||||
import com.r3corda.core.crypto.*
|
||||
import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.protocols.ProtocolLogic
|
||||
import com.r3corda.core.random63BitValue
|
||||
@ -121,17 +119,17 @@ object TwoPartyTradeProtocol {
|
||||
progressTracker.nextStep()
|
||||
|
||||
// Check that the tx proposed by the buyer is valid.
|
||||
val missingSigs = it.verify(throwIfSignaturesAreMissing = false)
|
||||
if (missingSigs != setOf(myKeyPair.public, notaryNode.identity.owningKey))
|
||||
throw SignatureException("The set of missing signatures is not as expected: $missingSigs")
|
||||
val missingSigs: Set<PublicKey> = it.verifySignatures(throwIfSignaturesAreMissing = false)
|
||||
val expected = setOf(myKeyPair.public, notaryNode.identity.owningKey)
|
||||
if (missingSigs != expected)
|
||||
throw SignatureException("The set of missing signatures is not as expected: ${missingSigs.toStringsShort()} vs [${myKeyPair.public.toStringShort()}, ${notaryNode.identity.owningKey.toStringShort()}]")
|
||||
|
||||
val wtx: WireTransaction = it.tx
|
||||
logger.trace { "Received partially signed transaction: ${it.id}" }
|
||||
|
||||
checkDependencies(it)
|
||||
|
||||
// This verifies that the transaction is contract-valid, even though it is missing signatures.
|
||||
serviceHub.verifyTransaction(wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments))
|
||||
// Download and check all the things that this transaction depends on and verify it is contract-valid,
|
||||
// even though it is missing signatures.
|
||||
subProtocol(ResolveTransactionsProtocol(wtx, otherSide))
|
||||
|
||||
if (wtx.outputs.map { it.data }.sumCashBy(myKeyPair.public).withoutIssuer() != price)
|
||||
throw IllegalArgumentException("Transaction is not sending us the right amount of cash")
|
||||
@ -150,14 +148,6 @@ object TwoPartyTradeProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun checkDependencies(stx: SignedTransaction) {
|
||||
// Download and check all the transactions that this transaction depends on, but do not check this
|
||||
// transaction itself.
|
||||
val dependencyTxIDs = stx.tx.inputs.map { it.txhash }.toSet()
|
||||
subProtocol(ResolveTransactionsProtocol(dependencyTxIDs, otherSide))
|
||||
}
|
||||
|
||||
open fun signWithOurKey(partialTX: SignedTransaction): DigitalSignature.WithKey {
|
||||
progressTracker.currentStep = SIGNING
|
||||
return myKeyPair.signWithECDSA(partialTX.txBits)
|
||||
@ -206,7 +196,7 @@ object TwoPartyTradeProtocol {
|
||||
logger.trace { "Got signatures from seller, verifying ... " }
|
||||
|
||||
val fullySigned = stx + signatures.sellerSig + signatures.notarySig
|
||||
fullySigned.verify()
|
||||
fullySigned.verifySignatures()
|
||||
|
||||
logger.trace { "Signatures received are valid. Trade complete! :-)" }
|
||||
return fullySigned
|
||||
|
@ -1,11 +1,12 @@
|
||||
package com.r3corda.contracts
|
||||
|
||||
import com.r3corda.contracts.asset.*
|
||||
import com.r3corda.contracts.testing.fillWithSomeTestCash
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.days
|
||||
import com.r3corda.core.node.services.testing.MockStorageService
|
||||
import com.r3corda.core.node.services.testing.MockServices
|
||||
import com.r3corda.core.seconds
|
||||
import com.r3corda.core.testing.*
|
||||
import org.junit.Test
|
||||
@ -72,7 +73,6 @@ class CommercialPaperTestsGeneric {
|
||||
@Parameterized.Parameter
|
||||
lateinit var thisTest: ICommercialPaperTestTemplate
|
||||
|
||||
val attachments = MockStorageService().attachments
|
||||
val issuer = MEGA_CORP.ref(123)
|
||||
|
||||
@Test
|
||||
@ -190,59 +190,62 @@ class CommercialPaperTestsGeneric {
|
||||
|
||||
@Test
|
||||
fun `issue move and then redeem`() {
|
||||
// MiniCorp issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself.
|
||||
val issueTX: LedgerTransaction = run {
|
||||
val ptx = CommercialPaper().generateIssue(MINI_CORP.ref(123), 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER,
|
||||
TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply {
|
||||
val aliceServices = MockServices()
|
||||
val alicesWallet = aliceServices.fillWithSomeTestCash(9000.DOLLARS)
|
||||
|
||||
val bigCorpServices = MockServices()
|
||||
val bigCorpWallet = bigCorpServices.fillWithSomeTestCash(13000.DOLLARS)
|
||||
|
||||
// Propagate the cash transactions to each side.
|
||||
aliceServices.recordTransactions(bigCorpWallet.states.map { bigCorpServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! })
|
||||
bigCorpServices.recordTransactions(alicesWallet.states.map { aliceServices.storageService.validatedTransactions.getTransaction(it.ref.txhash)!! })
|
||||
|
||||
// BigCorp™ issues $10,000 of commercial paper, to mature in 30 days, owned initially by itself.
|
||||
val faceValue = 10000.DOLLARS `issued by` DUMMY_CASH_ISSUER
|
||||
val issuance = bigCorpServices.storageService.myLegalIdentity.ref(1)
|
||||
val issueTX: SignedTransaction =
|
||||
CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply {
|
||||
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
signWith(MINI_CORP_KEY)
|
||||
signWith(bigCorpServices.key)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments)
|
||||
}
|
||||
}.toSignedTransaction()
|
||||
|
||||
val (alicesWalletTX, alicesWallet) = cashOutputsToWallet(
|
||||
3000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY,
|
||||
3000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY,
|
||||
3000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY
|
||||
)
|
||||
|
||||
// Alice pays $9000 to MiniCorp to own some of their debt.
|
||||
val moveTX: LedgerTransaction = run {
|
||||
// Alice pays $9000 to BigCorp to own some of their debt.
|
||||
val moveTX: SignedTransaction = run {
|
||||
val ptx = TransactionType.General.Builder()
|
||||
Cash().generateSpend(ptx, 9000.DOLLARS, MINI_CORP_PUBKEY, alicesWallet)
|
||||
CommercialPaper().generateMove(ptx, issueTX.outRef(0), ALICE_PUBKEY)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
ptx.signWith(ALICE_KEY)
|
||||
Cash().generateSpend(ptx, 9000.DOLLARS, bigCorpServices.key.public, alicesWallet.statesOfType<Cash.State>())
|
||||
CommercialPaper().generateMove(ptx, issueTX.tx.outRef(0), aliceServices.key.public)
|
||||
ptx.signWith(bigCorpServices.key)
|
||||
ptx.signWith(aliceServices.key)
|
||||
ptx.signWith(DUMMY_NOTARY_KEY)
|
||||
ptx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments)
|
||||
ptx.toSignedTransaction()
|
||||
}
|
||||
|
||||
// Won't be validated.
|
||||
val (corpWalletTX, corpWallet) = cashOutputsToWallet(
|
||||
9000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` MINI_CORP_PUBKEY `with notary` DUMMY_NOTARY,
|
||||
4000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` MINI_CORP_PUBKEY `with notary` DUMMY_NOTARY
|
||||
)
|
||||
|
||||
fun makeRedeemTX(time: Instant): LedgerTransaction {
|
||||
fun makeRedeemTX(time: Instant): SignedTransaction {
|
||||
val ptx = TransactionType.General.Builder()
|
||||
ptx.setTime(time, DUMMY_NOTARY, 30.seconds)
|
||||
CommercialPaper().generateRedeem(ptx, moveTX.outRef(1), corpWallet)
|
||||
ptx.signWith(ALICE_KEY)
|
||||
ptx.signWith(MINI_CORP_KEY)
|
||||
CommercialPaper().generateRedeem(ptx, moveTX.tx.outRef(1), bigCorpWallet.statesOfType<Cash.State>())
|
||||
ptx.signWith(aliceServices.key)
|
||||
ptx.signWith(bigCorpServices.key)
|
||||
ptx.signWith(DUMMY_NOTARY_KEY)
|
||||
return ptx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments)
|
||||
return ptx.toSignedTransaction()
|
||||
}
|
||||
|
||||
val tooEarlyRedemption = makeRedeemTX(TEST_TX_TIME + 10.days)
|
||||
val validRedemption = makeRedeemTX(TEST_TX_TIME + 31.days)
|
||||
|
||||
// Verify the txns are valid and insert into both sides.
|
||||
listOf(issueTX, moveTX).forEach {
|
||||
it.toLedgerTransaction(aliceServices).verify()
|
||||
aliceServices.recordTransactions(it)
|
||||
bigCorpServices.recordTransactions(it)
|
||||
}
|
||||
|
||||
val e = assertFailsWith(TransactionVerificationException::class) {
|
||||
TransactionGroup(setOf(issueTX, moveTX, tooEarlyRedemption), setOf(corpWalletTX, alicesWalletTX)).verify()
|
||||
tooEarlyRedemption.toLedgerTransaction(aliceServices).verify()
|
||||
}
|
||||
assertTrue(e.cause!!.message!!.contains("paper must have matured"))
|
||||
|
||||
TransactionGroup(setOf(issueTX, moveTX, validRedemption), setOf(corpWalletTX, alicesWalletTX)).verify()
|
||||
validRedemption.toLedgerTransaction(aliceServices).verify()
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package com.r3corda.contracts
|
||||
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.node.services.testing.MockStorageService
|
||||
import com.r3corda.core.node.services.testing.MockServices
|
||||
import com.r3corda.core.seconds
|
||||
import com.r3corda.core.testing.*
|
||||
import org.junit.Test
|
||||
@ -195,9 +195,6 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State {
|
||||
}
|
||||
|
||||
class IRSTests {
|
||||
|
||||
val attachments = MockStorageService().attachments
|
||||
|
||||
@Test
|
||||
fun ok() {
|
||||
trade().verifies()
|
||||
@ -211,9 +208,9 @@ class IRSTests {
|
||||
/**
|
||||
* Generate an IRS txn - we'll need it for a few things.
|
||||
*/
|
||||
fun generateIRSTxn(irsSelect: Int): LedgerTransaction {
|
||||
fun generateIRSTxn(irsSelect: Int): SignedTransaction {
|
||||
val dummyIRS = createDummyIRS(irsSelect)
|
||||
val genTX: LedgerTransaction = run {
|
||||
val genTX: SignedTransaction = run {
|
||||
val gtx = InterestRateSwap().generateAgreement(
|
||||
fixedLeg = dummyIRS.fixedLeg,
|
||||
floatingLeg = dummyIRS.floatingLeg,
|
||||
@ -225,7 +222,7 @@ class IRSTests {
|
||||
signWith(MINI_CORP_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
gtx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments)
|
||||
gtx.toSignedTransaction()
|
||||
}
|
||||
return genTX
|
||||
}
|
||||
@ -243,7 +240,7 @@ class IRSTests {
|
||||
* Utility so I don't have to keep typing this.
|
||||
*/
|
||||
fun singleIRS(irsSelector: Int = 1): InterestRateSwap.State {
|
||||
return generateIRSTxn(irsSelector).outputs.map { it.data }.filterIsInstance<InterestRateSwap.State>().single()
|
||||
return generateIRSTxn(irsSelector).tx.outputs.map { it.data }.filterIsInstance<InterestRateSwap.State>().single()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -293,30 +290,30 @@ class IRSTests {
|
||||
*/
|
||||
@Test
|
||||
fun generateIRSandFixSome() {
|
||||
val services = MockServices()
|
||||
var previousTXN = generateIRSTxn(1)
|
||||
fun currentIRS() = previousTXN.outputs.map { it.data }.filterIsInstance<InterestRateSwap.State>().single()
|
||||
|
||||
val txns = HashSet<LedgerTransaction>()
|
||||
txns += previousTXN
|
||||
previousTXN.toLedgerTransaction(services).verify()
|
||||
services.recordTransactions(previousTXN)
|
||||
fun currentIRS() = previousTXN.tx.outputs.map { it.data }.filterIsInstance<InterestRateSwap.State>().single()
|
||||
|
||||
while (true) {
|
||||
val nextFix: FixOf = currentIRS().nextFixingOf() ?: break
|
||||
val fixTX: LedgerTransaction = run {
|
||||
val fixTX: SignedTransaction = run {
|
||||
val tx = TransactionType.General.Builder()
|
||||
val fixing = Fix(nextFix, "0.052".percent.value)
|
||||
InterestRateSwap().generateFix(tx, previousTXN.outRef(0), fixing)
|
||||
InterestRateSwap().generateFix(tx, previousTXN.tx.outRef(0), fixing)
|
||||
with(tx) {
|
||||
setTime(TEST_TX_TIME, DUMMY_NOTARY, 30.seconds)
|
||||
signWith(MEGA_CORP_KEY)
|
||||
signWith(MINI_CORP_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
tx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, attachments)
|
||||
tx.toSignedTransaction()
|
||||
}
|
||||
fixTX.toLedgerTransaction(services).verify()
|
||||
services.recordTransactions(fixTX)
|
||||
previousTXN = fixTX
|
||||
txns += fixTX
|
||||
}
|
||||
TransactionGroup(txns, emptySet()).verify()
|
||||
}
|
||||
|
||||
// Move these later as they aren't IRS specific.
|
||||
|
@ -289,7 +289,7 @@ class ObligationTests {
|
||||
}.toSignedTransaction()
|
||||
assertEquals(1, tx.tx.outputs.size)
|
||||
assertEquals(stateAndRef.state.data.copy(lifecycle = Lifecycle.DEFAULTED), tx.tx.outputs[0].data)
|
||||
assertTrue(tx.verify().isEmpty())
|
||||
tx.verifySignatures()
|
||||
|
||||
// And set it back
|
||||
stateAndRef = tx.tx.outRef<Obligation.State<Currency>>(0)
|
||||
@ -300,7 +300,7 @@ class ObligationTests {
|
||||
}.toSignedTransaction()
|
||||
assertEquals(1, tx.tx.outputs.size)
|
||||
assertEquals(stateAndRef.state.data.copy(lifecycle = Lifecycle.NORMAL), tx.tx.outputs[0].data)
|
||||
assertTrue(tx.verify().isEmpty())
|
||||
tx.verifySignatures()
|
||||
}
|
||||
|
||||
/** Test generating a transaction to settle an obligation. */
|
||||
|
@ -1,32 +1,39 @@
|
||||
package com.r3corda.core.contracts
|
||||
|
||||
import com.r3corda.core.node.services.AttachmentStorage
|
||||
import com.r3corda.core.node.services.IdentityService
|
||||
import com.r3corda.core.node.ServiceHub
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
// TODO: Move these into the actual classes (i.e. where people would expect to find them) and split Transactions.kt into multiple files
|
||||
|
||||
/**
|
||||
* Looks up identities and attachments from storage to generate a [LedgerTransaction].
|
||||
* Looks up identities and attachments from storage to generate a [LedgerTransaction]. A transaction is expected to
|
||||
* have been fully resolved using the resolution protocol by this point.
|
||||
*
|
||||
* @throws FileNotFoundException if a required attachment was not found in storage.
|
||||
* @throws TransactionResolutionException if an input points to a transaction not found in storage.
|
||||
*/
|
||||
fun WireTransaction.toLedgerTransaction(identityService: IdentityService,
|
||||
attachmentStorage: AttachmentStorage): LedgerTransaction {
|
||||
fun WireTransaction.toLedgerTransaction(services: ServiceHub): LedgerTransaction {
|
||||
// Look up random keys to authenticated identities. This is just a stub placeholder and will all change in future.
|
||||
val authenticatedArgs = commands.map {
|
||||
val institutions = it.signers.mapNotNull { pk -> identityService.partyFromKey(pk) }
|
||||
AuthenticatedObject(it.signers, institutions, it.value)
|
||||
val parties = it.signers.mapNotNull { pk -> services.identityService.partyFromKey(pk) }
|
||||
AuthenticatedObject(it.signers, parties, it.value)
|
||||
}
|
||||
// Open attachments specified in this transaction. If we haven't downloaded them, we fail.
|
||||
val attachments = attachments.map {
|
||||
attachmentStorage.openAttachment(it) ?: throw FileNotFoundException(it.toString())
|
||||
services.storageService.attachments.openAttachment(it) ?: throw FileNotFoundException(it.toString())
|
||||
}
|
||||
return LedgerTransaction(inputs, outputs, authenticatedArgs, attachments, id, signers, type)
|
||||
val resolvedInputs = inputs.map { StateAndRef(services.loadState(it), it) }
|
||||
return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, signers, type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls [verify] to check all required signatures are present, and then uses the passed [IdentityService] to call
|
||||
* [WireTransaction.toLedgerTransaction] to look up well known identities from pubkeys.
|
||||
* Calls [verify] to check all required signatures are present, and then calls [WireTransaction.toLedgerTransaction]
|
||||
* with the passed in [ServiceHub] to resolve the dependencies, returning an unverified LedgerTransaction.
|
||||
*
|
||||
* @throws FileNotFoundException if a required attachment was not found in storage.
|
||||
* @throws TransactionResolutionException if an input points to a transaction not found in storage.
|
||||
*/
|
||||
fun SignedTransaction.verifyToLedgerTransaction(identityService: IdentityService,
|
||||
attachmentStorage: AttachmentStorage): LedgerTransaction {
|
||||
verify()
|
||||
return tx.toLedgerTransaction(identityService, attachmentStorage)
|
||||
fun SignedTransaction.toLedgerTransaction(services: ServiceHub): LedgerTransaction {
|
||||
verifySignatures()
|
||||
return tx.toLedgerTransaction(services)
|
||||
}
|
||||
|
@ -16,18 +16,17 @@ sealed class TransactionType {
|
||||
*
|
||||
* Note: Presence of _signatures_ is not checked, only the public keys to be signed for.
|
||||
*/
|
||||
fun verify(tx: TransactionForVerification) {
|
||||
fun verify(tx: LedgerTransaction) {
|
||||
val missing = verifySigners(tx)
|
||||
if (missing.isNotEmpty()) throw TransactionVerificationException.SignersMissing(tx, missing.toList())
|
||||
|
||||
verifyTransaction(tx)
|
||||
}
|
||||
|
||||
/** Check that the list of signers includes all the necessary keys */
|
||||
fun verifySigners(tx: TransactionForVerification): Set<PublicKey> {
|
||||
fun verifySigners(tx: LedgerTransaction): Set<PublicKey> {
|
||||
val timestamp = tx.commands.noneOrSingle { it.value is TimestampCommand }
|
||||
val timestampKey = timestamp?.signers.orEmpty()
|
||||
val notaryKey = (tx.inputs.map { it.notary.owningKey } + timestampKey).toSet()
|
||||
val notaryKey = (tx.inputs.map { it.state.notary.owningKey } + timestampKey).toSet()
|
||||
if (notaryKey.size > 1) throw TransactionVerificationException.MoreThanOneNotary(tx)
|
||||
|
||||
val requiredKeys = getRequiredSigners(tx) + notaryKey
|
||||
@ -40,10 +39,10 @@ sealed class TransactionType {
|
||||
* Return the list of public keys that that require signatures for the transaction type.
|
||||
* Note: the notary key is checked separately for all transactions and need not be included.
|
||||
*/
|
||||
abstract fun getRequiredSigners(tx: TransactionForVerification): Set<PublicKey>
|
||||
abstract fun getRequiredSigners(tx: LedgerTransaction): Set<PublicKey>
|
||||
|
||||
/** Implement type specific transaction validation logic */
|
||||
abstract fun verifyTransaction(tx: TransactionForVerification)
|
||||
abstract fun verifyTransaction(tx: LedgerTransaction)
|
||||
|
||||
/** A general transaction type where transaction validity is determined by custom contract code */
|
||||
class General : TransactionType() {
|
||||
@ -54,10 +53,11 @@ sealed class TransactionType {
|
||||
* Check the transaction is contract-valid by running the verify() for each input and output state contract.
|
||||
* If any contract fails to verify, the whole transaction is considered to be invalid.
|
||||
*/
|
||||
override fun verifyTransaction(tx: TransactionForVerification) {
|
||||
override fun verifyTransaction(tx: LedgerTransaction) {
|
||||
// TODO: Check that notary is unchanged
|
||||
val ctx = tx.toTransactionForContract()
|
||||
|
||||
// TODO: This will all be replaced in future once the sandbox and contract constraints work is done.
|
||||
val contracts = (ctx.inputs.map { it.contract } + ctx.outputs.map { it.contract }).toSet()
|
||||
for (contract in contracts) {
|
||||
try {
|
||||
@ -68,10 +68,7 @@ sealed class TransactionType {
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRequiredSigners(tx: TransactionForVerification): Set<PublicKey> {
|
||||
val commandKeys = tx.commands.flatMap { it.signers }.toSet()
|
||||
return commandKeys
|
||||
}
|
||||
override fun getRequiredSigners(tx: LedgerTransaction) = tx.commands.flatMap { it.signers }.toSet()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,14 +88,16 @@ sealed class TransactionType {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the difference between inputs and outputs is only the notary field,
|
||||
* and that all required signing public keys are present.
|
||||
* Check that the difference between inputs and outputs is only the notary field, and that all required signing
|
||||
* public keys are present.
|
||||
*
|
||||
* @throws TransactionVerificationException.InvalidNotaryChange if the validity check fails.
|
||||
*/
|
||||
override fun verifyTransaction(tx: TransactionForVerification) {
|
||||
override fun verifyTransaction(tx: LedgerTransaction) {
|
||||
try {
|
||||
tx.inputs.zip(tx.outputs).forEach {
|
||||
check(it.first.data == it.second.data)
|
||||
check(it.first.notary != it.second.notary)
|
||||
for ((input, output) in tx.inputs.zip(tx.outputs)) {
|
||||
check(input.state.data == output.data)
|
||||
check(input.state.notary != output.notary)
|
||||
}
|
||||
check(tx.commands.isEmpty())
|
||||
} catch (e: IllegalStateException) {
|
||||
@ -106,9 +105,6 @@ sealed class TransactionType {
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRequiredSigners(tx: TransactionForVerification): Set<PublicKey> {
|
||||
val participantKeys = tx.inputs.flatMap { it.data.participants }.toSet()
|
||||
return participantKeys
|
||||
}
|
||||
override fun getRequiredSigners(tx: LedgerTransaction) = tx.inputs.flatMap { it.state.data.participants }.toSet()
|
||||
}
|
||||
}
|
||||
|
@ -8,50 +8,6 @@ import java.util.*
|
||||
|
||||
// TODO: Consider moving this out of the core module and providing a different way for unit tests to test contracts.
|
||||
|
||||
/**
|
||||
* A TransactionGroup defines a directed acyclic graph of transactions that can be resolved with each other and then
|
||||
* verified. Successful verification does not imply the non-existence of other conflicting transactions: simply that
|
||||
* this subgraph does not contain conflicts and is accepted by the involved contracts.
|
||||
*
|
||||
* The inputs of the provided transactions must be resolvable either within the [transactions] set, or from the
|
||||
* [nonVerifiedRoots] set. Transactions in the non-verified set are ignored other than for looking up input states.
|
||||
*/
|
||||
class TransactionGroup(val transactions: Set<LedgerTransaction>, val nonVerifiedRoots: Set<LedgerTransaction>) {
|
||||
/**
|
||||
* Verifies the group and returns the set of resolved transactions.
|
||||
*/
|
||||
fun verify(): Set<TransactionForVerification> {
|
||||
// Check that every input can be resolved to an output.
|
||||
// Check that no output is referenced by more than one input.
|
||||
// Cycles should be impossible due to the use of hashes as pointers.
|
||||
check(transactions.intersect(nonVerifiedRoots).isEmpty())
|
||||
|
||||
val hashToTXMap: Map<SecureHash, List<LedgerTransaction>> = (transactions + nonVerifiedRoots).groupBy { it.id }
|
||||
val refToConsumingTXMap = hashMapOf<StateRef, LedgerTransaction>()
|
||||
|
||||
val resolved = HashSet<TransactionForVerification>(transactions.size)
|
||||
for (tx in transactions) {
|
||||
val inputs = ArrayList<TransactionState<ContractState>>(tx.inputs.size)
|
||||
for (ref in tx.inputs) {
|
||||
val conflict = refToConsumingTXMap[ref]
|
||||
if (conflict != null)
|
||||
throw TransactionConflictException(ref, tx, conflict)
|
||||
refToConsumingTXMap[ref] = tx
|
||||
|
||||
// Look up the connecting transaction.
|
||||
val ltx = hashToTXMap[ref.txhash]?.single() ?: throw TransactionResolutionException(ref.txhash)
|
||||
// Look up the output in that transaction by index.
|
||||
inputs.add(ltx.outputs[ref.index])
|
||||
}
|
||||
resolved.add(TransactionForVerification(inputs, tx.outputs, tx.attachments, tx.commands, tx.id, tx.signers, tx.type))
|
||||
}
|
||||
|
||||
for (tx in resolved)
|
||||
tx.verify()
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
|
||||
/** A transaction in fully resolved and sig-checked form, ready for passing as input to a verification function. */
|
||||
data class TransactionForVerification(val inputs: List<TransactionState<ContractState>>,
|
||||
val outputs: List<TransactionState<ContractState>>,
|
||||
@ -62,20 +18,6 @@ data class TransactionForVerification(val inputs: List<TransactionState<Contract
|
||||
val type: TransactionType) {
|
||||
override fun hashCode() = origHash.hashCode()
|
||||
override fun equals(other: Any?) = other is TransactionForVerification && other.origHash == origHash
|
||||
|
||||
/**
|
||||
* Verifies that the transaction is valid by running type-specific validation logic.
|
||||
*
|
||||
* TODO: Move this out of the core data structure definitions, once unit tests are more cleanly separated.
|
||||
*
|
||||
* @throws TransactionVerificationException if validation logic fails or if a contract throws an exception
|
||||
* (the original is in the cause field).
|
||||
*/
|
||||
@Throws(TransactionVerificationException::class)
|
||||
fun verify() = type.verify(this)
|
||||
|
||||
fun toTransactionForContract() = TransactionForContract(inputs.map { it.data }, outputs.map { it.data },
|
||||
attachments, commands, origHash, inputs.map { it.notary }.singleOrNull())
|
||||
}
|
||||
|
||||
/**
|
||||
@ -171,14 +113,16 @@ data class TransactionForContract(val inputs: List<ContractState>,
|
||||
fun getTimestampByName(vararg authorityName: String): TimestampCommand? = commands.getTimestampByName(*authorityName)
|
||||
}
|
||||
|
||||
class TransactionResolutionException(val hash: SecureHash) : Exception()
|
||||
class TransactionResolutionException(val hash: SecureHash) : Exception() {
|
||||
override fun toString() = "Transaction resolution failure for $hash"
|
||||
}
|
||||
class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception()
|
||||
|
||||
sealed class TransactionVerificationException(val tx: TransactionForVerification, cause: Throwable?) : Exception(cause) {
|
||||
class ContractRejection(tx: TransactionForVerification, val contract: Contract, cause: Throwable?) : TransactionVerificationException(tx, cause)
|
||||
class MoreThanOneNotary(tx: TransactionForVerification) : TransactionVerificationException(tx, null)
|
||||
class SignersMissing(tx: TransactionForVerification, val missing: List<PublicKey>) : TransactionVerificationException(tx, null) {
|
||||
sealed class TransactionVerificationException(val tx: LedgerTransaction, cause: Throwable?) : Exception(cause) {
|
||||
class ContractRejection(tx: LedgerTransaction, val contract: Contract, cause: Throwable?) : TransactionVerificationException(tx, cause)
|
||||
class MoreThanOneNotary(tx: LedgerTransaction) : TransactionVerificationException(tx, null)
|
||||
class SignersMissing(tx: LedgerTransaction, val missing: List<PublicKey>) : TransactionVerificationException(tx, null) {
|
||||
override fun toString() = "Signers missing: ${missing.map { it.toStringShort() }}"
|
||||
}
|
||||
class InvalidNotaryChange(tx: TransactionForVerification) : TransactionVerificationException(tx, null)
|
||||
class InvalidNotaryChange(tx: LedgerTransaction) : TransactionVerificationException(tx, null)
|
||||
}
|
||||
|
@ -21,17 +21,23 @@ import java.security.SignatureException
|
||||
* a public key that is mentioned inside a transaction command. SignedTransaction is the top level transaction type
|
||||
* and the type most frequently passed around the network and stored. The identity of a transaction is the hash
|
||||
* of a WireTransaction, therefore if you are storing data keyed by WT hash be aware that multiple different SWTs may
|
||||
* map to the same key (and they could be different in important ways!).
|
||||
* map to the same key (and they could be different in important ways, like validity!). The signatures on a
|
||||
* SignedTransaction might be invalid or missing: the type does not imply validity.
|
||||
*
|
||||
* WireTransaction is a transaction in a form ready to be serialised/unserialised. A WireTransaction can be hashed
|
||||
* in various ways to calculate a *signature hash* (or sighash), this is the hash that is signed by the various involved
|
||||
* keypairs. Note that a sighash is not the same thing as a *transaction id*, which is the hash of the entire
|
||||
* WireTransaction i.e. the outermost serialised form with everything included.
|
||||
* keypairs. A WireTransaction can be safely deserialised from inside a SignedTransaction outside of the sandbox,
|
||||
* because it consists of only platform defined types. Any user defined object graphs are kept as byte arrays. A
|
||||
* WireTransaction may be invalid and missing its dependencies (other transactions + attachments).
|
||||
*
|
||||
* LedgerTransaction is derived from WireTransaction. It is the result of doing some basic key lookups on WireCommand
|
||||
* to see if any keys are from a recognised party, thus converting the WireCommand objects into
|
||||
* AuthenticatedObject<Command>. Currently we just assume a hard coded pubkey->party map. In future it'd make more
|
||||
* sense to use a certificate scheme and so that logic would get more complex.
|
||||
* LedgerTransaction is derived from WireTransaction. It is the result of doing the following operations:
|
||||
*
|
||||
* - Downloading and locally storing all the dependencies of the transaction.
|
||||
* - Doing some basic key lookups on WireCommand to see if any keys are from a recognised party, thus converting the
|
||||
* WireCommand objects into AuthenticatedObject<Command>. Currently we just assume a hard coded pubkey->party map.
|
||||
* In future it'd make more sense to use a certificate scheme and so that logic would get more complex.
|
||||
* - Deserialising the included states, sandboxing the contract classes, and generally ensuring the embedded code is
|
||||
* safe for interaction.
|
||||
*
|
||||
* All the above refer to inputs using a (txhash, output index) pair.
|
||||
*
|
||||
@ -73,7 +79,7 @@ data class WireTransaction(val inputs: List<StateRef>,
|
||||
|
||||
override fun toString(): String {
|
||||
val buf = StringBuilder()
|
||||
buf.appendln("Transaction:")
|
||||
buf.appendln("Transaction $id:")
|
||||
for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input")
|
||||
for (output in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $output")
|
||||
for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command")
|
||||
@ -97,18 +103,6 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
|
||||
/** A transaction ID is the hash of the [WireTransaction]. Thus adding or removing a signature does not change it. */
|
||||
override val id: SecureHash get() = txBits.hash
|
||||
|
||||
/**
|
||||
* Verifies the given signatures against the serialized transaction data. Does NOT deserialise or check the contents
|
||||
* to ensure there are no missing signatures: use verify() to do that. This weaker version can be useful for
|
||||
* checking a partially signed transaction being prepared by multiple co-operating parties.
|
||||
*
|
||||
* @throws SignatureException if the signature is invalid or does not match.
|
||||
*/
|
||||
fun verifySignatures() {
|
||||
for (sig in sigs)
|
||||
sig.verifyWithECDSA(txBits.bits)
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the signatures, deserialise the wire transaction and then check that the set of signatures found contains
|
||||
* the set of pubkeys in the signers list. If any signatures are missing, either throws an exception (by default) or
|
||||
@ -116,9 +110,12 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
|
||||
*
|
||||
* @throws SignatureException if a signature is invalid, does not match or if any signature is missing.
|
||||
*/
|
||||
fun verify(throwIfSignaturesAreMissing: Boolean = true): Set<PublicKey> {
|
||||
verifySignatures()
|
||||
fun verifySignatures(throwIfSignaturesAreMissing: Boolean = true): Set<PublicKey> {
|
||||
// Embedded WireTransaction is not deserialised until after we check the signatures.
|
||||
for (sig in sigs)
|
||||
sig.verifyWithECDSA(txBits.bits)
|
||||
|
||||
// Now examine the contents and ensure the sigs we have line up with the advertised list of signers.
|
||||
val missing = getMissingSignatures()
|
||||
if (missing.isNotEmpty() && throwIfSignaturesAreMissing)
|
||||
throw SignatureException("Missing signatures on transaction ${id.prefixChars()} for: ${missing.map { it.toStringShort() }}")
|
||||
@ -157,12 +154,10 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
|
||||
* A LedgerTransaction wraps the data needed to calculate one or more successor states from a set of input states.
|
||||
* It is the first step after extraction from a WireTransaction. The signatures at this point have been lined up
|
||||
* with the commands from the wire, and verified/looked up.
|
||||
*
|
||||
* TODO: This class needs a bit more thought. Should inputs be fully resolved by this point too?
|
||||
*/
|
||||
data class LedgerTransaction(
|
||||
/** The input states which will be consumed/invalidated by the execution of this transaction. */
|
||||
val inputs: List<StateRef>,
|
||||
val inputs: List<StateAndRef<*>>,
|
||||
/** The states that will be generated by the execution of this transaction. */
|
||||
val outputs: List<TransactionState<*>>,
|
||||
/** Arbitrary data passed to the program of each input state. */
|
||||
@ -171,9 +166,28 @@ data class LedgerTransaction(
|
||||
val attachments: List<Attachment>,
|
||||
/** The hash of the original serialised WireTransaction */
|
||||
override val id: SecureHash,
|
||||
/** The notary key and the command keys together: a signed transaction must provide signatures for all of these. */
|
||||
val signers: List<PublicKey>,
|
||||
val type: TransactionType
|
||||
) : NamedByHash {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : ContractState> outRef(index: Int) = StateAndRef(outputs[index] as TransactionState<T>, StateRef(id, index))
|
||||
}
|
||||
|
||||
// TODO: Remove this concept.
|
||||
// There isn't really a good justification for hiding this data from the contract, it's just a backwards compat hack.
|
||||
/** Strips the transaction down to a form that is usable by the contract verify functions */
|
||||
fun toTransactionForContract(): TransactionForContract {
|
||||
return TransactionForContract(inputs.map { it.state.data }, outputs.map { it.data }, attachments, commands, id,
|
||||
inputs.map { it.state.notary }.singleOrNull())
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies this transaction and throws an exception if not valid, depending on the type. For general transactions:
|
||||
*
|
||||
* - The contracts are run with the transaction as the input.
|
||||
* - The list of keys mentioned in commands is compared against the signers list.
|
||||
*
|
||||
* @throws TransactionVerificationException if anything goes wrong.
|
||||
*/
|
||||
fun verify() = type.verify(this)
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
package com.r3corda.core.node
|
||||
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.contracts.SignedTransaction
|
||||
import com.r3corda.core.contracts.StateRef
|
||||
import com.r3corda.core.contracts.TransactionResolutionException
|
||||
import com.r3corda.core.contracts.TransactionState
|
||||
import com.r3corda.core.messaging.MessagingService
|
||||
import com.r3corda.core.node.services.*
|
||||
import com.r3corda.core.protocols.ProtocolLogic
|
||||
@ -25,19 +28,6 @@ interface ServiceHub {
|
||||
val schedulerService: SchedulerService
|
||||
val clock: Clock
|
||||
|
||||
/**
|
||||
* Given a [LedgerTransaction], looks up all its dependencies in the local database, uses the identity service to map
|
||||
* the [SignedTransaction]s the DB gives back into [LedgerTransaction]s, and then runs the smart contracts for the
|
||||
* transaction. If no exception is thrown, the transaction is valid.
|
||||
*/
|
||||
fun verifyTransaction(ltx: LedgerTransaction) {
|
||||
val dependencies = ltx.inputs.map {
|
||||
storageService.validatedTransactions.getTransaction(it.txhash) ?: throw TransactionResolutionException(it.txhash)
|
||||
}
|
||||
val ltxns = dependencies.map { it.verifyToLedgerTransaction(identityService, storageService.attachments) }
|
||||
TransactionGroup(setOf(ltx), ltxns.toSet()).verify()
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of [SignedTransaction]s, writes them to the local storage for validated transactions and then
|
||||
* sends them to the wallet for further processing.
|
||||
@ -70,4 +60,4 @@ interface ServiceHub {
|
||||
* @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>
|
||||
}
|
||||
}
|
@ -1,10 +1,7 @@
|
||||
package com.r3corda.core.testing
|
||||
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.DigitalSignature
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.crypto.signWithECDSA
|
||||
import com.r3corda.core.crypto.*
|
||||
import com.r3corda.core.node.ServiceHub
|
||||
import com.r3corda.core.serialization.serialize
|
||||
import java.io.InputStream
|
||||
@ -133,8 +130,7 @@ data class TestTransactionDSLInterpreter private constructor(
|
||||
}
|
||||
|
||||
override fun verifies(): EnforceVerifyOrFail {
|
||||
val resolvedTransaction = ledgerInterpreter.resolveWireTransaction(toWireTransaction())
|
||||
resolvedTransaction.verify()
|
||||
toWireTransaction().toLedgerTransaction(services).verify()
|
||||
return EnforceVerifyOrFail.Token
|
||||
}
|
||||
|
||||
@ -185,26 +181,6 @@ data class TestLedgerDSLInterpreter private constructor (
|
||||
nonVerifiedTransactionWithLocations = HashMap(nonVerifiedTransactionWithLocations)
|
||||
)
|
||||
|
||||
internal fun resolveWireTransaction(wireTransaction: WireTransaction): TransactionForVerification {
|
||||
return wireTransaction.run {
|
||||
val authenticatedCommands = commands.map {
|
||||
AuthenticatedObject(it.signers, it.signers.mapNotNull { services.identityService.partyFromKey(it) }, it.value)
|
||||
}
|
||||
val resolvedInputStates = inputs.map { resolveStateRef<ContractState>(it) }
|
||||
val resolvedAttachments = attachments.map { resolveAttachment(it) }
|
||||
TransactionForVerification(
|
||||
inputs = resolvedInputStates,
|
||||
outputs = outputs,
|
||||
commands = authenticatedCommands,
|
||||
origHash = wireTransaction.serialized.hash,
|
||||
attachments = resolvedAttachments,
|
||||
signers = signers.toList(),
|
||||
type = type
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun <reified S : ContractState> resolveStateRef(stateRef: StateRef): TransactionState<S> {
|
||||
val transactionWithLocation =
|
||||
transactionWithLocations[stateRef.txhash] ?:
|
||||
@ -230,16 +206,6 @@ data class TestLedgerDSLInterpreter private constructor (
|
||||
return transactionInterpreter
|
||||
}
|
||||
|
||||
fun toTransactionGroup(): TransactionGroup {
|
||||
val ledgerTransactions = transactionWithLocations.map {
|
||||
it.value.transaction.toLedgerTransaction(services.identityService, services.storageService.attachments)
|
||||
}
|
||||
val nonVerifiedLedgerTransactions = nonVerifiedTransactionWithLocations.map {
|
||||
it.value.transaction.toLedgerTransaction(services.identityService, services.storageService.attachments)
|
||||
}
|
||||
return TransactionGroup(ledgerTransactions.toSet(), nonVerifiedLedgerTransactions.toSet())
|
||||
}
|
||||
|
||||
fun transactionName(transactionHash: SecureHash): String? {
|
||||
val transactionWithLocation = transactionWithLocations[transactionHash]
|
||||
return if (transactionWithLocation != null) {
|
||||
@ -298,16 +264,18 @@ data class TestLedgerDSLInterpreter private constructor (
|
||||
}
|
||||
|
||||
override fun verifies(): EnforceVerifyOrFail {
|
||||
val transactionGroup = toTransactionGroup()
|
||||
try {
|
||||
transactionGroup.verify()
|
||||
services.recordTransactions(transactionsUnverified.map { SignedTransaction(it.serialized, listOf(NullSignature)) })
|
||||
for ((key, value) in transactionWithLocations) {
|
||||
value.transaction.toLedgerTransaction(services).verify()
|
||||
services.recordTransactions(SignedTransaction(value.transaction.serialized, listOf(NullSignature)))
|
||||
}
|
||||
return EnforceVerifyOrFail.Token
|
||||
} catch (exception: TransactionVerificationException) {
|
||||
val transactionWithLocation = transactionWithLocations[exception.tx.origHash]
|
||||
val transactionWithLocation = transactionWithLocations[exception.tx.id]
|
||||
val transactionName = transactionWithLocation?.label ?: transactionWithLocation?.location ?: "<unknown>"
|
||||
throw VerifiesFailed(transactionName, exception)
|
||||
}
|
||||
|
||||
return EnforceVerifyOrFail.Token
|
||||
}
|
||||
|
||||
override fun <S : ContractState> retrieveOutputStateAndRef(clazz: Class<S>, label: String): StateAndRef<S> {
|
||||
|
@ -10,9 +10,8 @@ import com.r3corda.core.node.NodeInfo
|
||||
import com.r3corda.core.protocols.ProtocolLogic
|
||||
import com.r3corda.core.random63BitValue
|
||||
import com.r3corda.core.utilities.ProgressTracker
|
||||
import com.r3corda.protocols.NotaryProtocol
|
||||
import com.r3corda.protocols.PartyRequestMessage
|
||||
import com.r3corda.protocols.ResolveTransactionsProtocol
|
||||
import com.r3corda.protocols.AbstractStateReplacementProtocol.Acceptor
|
||||
import com.r3corda.protocols.AbstractStateReplacementProtocol.Instigator
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
@ -164,13 +163,14 @@ abstract class AbstractStateReplacementProtocol<T> {
|
||||
val response = Result.noError(mySignature)
|
||||
val swapSignatures = sendAndReceive<List<DigitalSignature.WithKey>>(otherSide, sessionIdForSend, sessionIdForReceive, response)
|
||||
|
||||
// TODO: This step should not be necessary, as signatures are re-checked in verifySignatures.
|
||||
val allSignatures = swapSignatures.validate { signatures ->
|
||||
signatures.forEach { it.verifyWithECDSA(stx.txBits) }
|
||||
signatures
|
||||
}
|
||||
|
||||
val finalTx = stx + allSignatures
|
||||
finalTx.verify()
|
||||
finalTx.verifySignatures()
|
||||
serviceHub.recordTransactions(listOf(finalTx))
|
||||
}
|
||||
|
||||
@ -191,7 +191,9 @@ abstract class AbstractStateReplacementProtocol<T> {
|
||||
private fun verifyTx(stx: SignedTransaction) {
|
||||
checkMySignatureRequired(stx.tx)
|
||||
checkDependenciesValid(stx)
|
||||
checkValid(stx)
|
||||
// We expect stx to have insufficient signatures, so we convert the WireTransaction to the LedgerTransaction
|
||||
// here, thus bypassing the sufficient-signatures check.
|
||||
stx.tx.toLedgerTransaction(serviceHub).verify()
|
||||
}
|
||||
|
||||
private fun checkMySignatureRequired(tx: WireTransaction) {
|
||||
@ -202,19 +204,10 @@ abstract class AbstractStateReplacementProtocol<T> {
|
||||
|
||||
@Suspendable
|
||||
private fun checkDependenciesValid(stx: SignedTransaction) {
|
||||
val dependencyTxIDs = stx.tx.inputs.map { it.txhash }.toSet()
|
||||
subProtocol(ResolveTransactionsProtocol(dependencyTxIDs, otherSide))
|
||||
subProtocol(ResolveTransactionsProtocol(stx.tx, otherSide))
|
||||
}
|
||||
|
||||
private fun checkValid(stx: SignedTransaction) {
|
||||
val ltx = stx.tx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
|
||||
serviceHub.verifyTransaction(ltx)
|
||||
}
|
||||
|
||||
private fun sign(stx: SignedTransaction): DigitalSignature.WithKey {
|
||||
val myKeyPair = serviceHub.storageService.myLegalIdentityKey
|
||||
return myKeyPair.signWithECDSA(stx.txBits)
|
||||
}
|
||||
private fun sign(stx: SignedTransaction) = serviceHub.storageService.myLegalIdentityKey.signWithECDSA(stx.txBits)
|
||||
}
|
||||
|
||||
// TODO: similar classes occur in other places (NotaryProtocol), need to consolidate
|
||||
|
@ -1,27 +1,35 @@
|
||||
package com.r3corda.protocols
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.checkedAdd
|
||||
import com.r3corda.core.contracts.LedgerTransaction
|
||||
import com.r3corda.core.contracts.SignedTransaction
|
||||
import com.r3corda.core.contracts.WireTransaction
|
||||
import com.r3corda.core.contracts.toLedgerTransaction
|
||||
import com.r3corda.core.crypto.Party
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.protocols.ProtocolLogic
|
||||
import java.util.*
|
||||
|
||||
// NB: This code is unit tested by TwoPartyTradeProtocolTests
|
||||
// TODO: This code is currently unit tested by TwoPartyTradeProtocolTests, it should have its own tests.
|
||||
|
||||
// TODO: It may be a clearer API if we make the primary c'tor private here, and only allow a single tx to be "resolved".
|
||||
|
||||
/**
|
||||
* This protocol fetches each transaction identified by the given hashes from either disk or network, along with all
|
||||
* their dependencies, and verifies them together using a single [TransactionGroup]. If no exception is thrown, then
|
||||
* all the transactions have been successfully verified and inserted into the local database.
|
||||
* This protocol is used to verify the validity of a transaction by recursively checking the validity of all the
|
||||
* dependencies. Once a transaction is checked it's inserted into local storage so it can be relayed and won't be
|
||||
* checked again.
|
||||
*
|
||||
* A couple of constructors are provided that accept a single transaction. When these are used, the dependencies of that
|
||||
* transaction are resolved and then the transaction itself is verified. Again, if successful, the results are inserted
|
||||
* into the database as long as a [SignedTransaction] was provided. If only the [WireTransaction] form was provided
|
||||
* then this isn't enough to put into the local database, so only the dependencies are inserted. This way to use the
|
||||
* protocol is helpful when resolving and verifying a finished but partially signed transaction.
|
||||
* then this isn't enough to put into the local database, so only the dependencies are checked and inserted. This way
|
||||
* to use the protocol is helpful when resolving and verifying a finished but partially signed transaction.
|
||||
*
|
||||
* The protocol returns a list of verified [LedgerTransaction] objects, in a depth-first order.
|
||||
*/
|
||||
class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
|
||||
private val otherSide: Party) : ProtocolLogic<Unit>() {
|
||||
private val otherSide: Party) : ProtocolLogic<List<LedgerTransaction>>() {
|
||||
|
||||
companion object {
|
||||
private fun dependencyIDs(wtx: WireTransaction) = wtx.inputs.map { it.txhash }.toSet()
|
||||
@ -48,45 +56,49 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): Unit {
|
||||
val toVerify = HashSet<LedgerTransaction>()
|
||||
val alreadyVerified = HashSet<LedgerTransaction>()
|
||||
val downloadedSignedTxns = ArrayList<SignedTransaction>()
|
||||
override fun call(): List<LedgerTransaction> {
|
||||
val newTxns: Iterable<SignedTransaction> = downloadDependencies(txHashes)
|
||||
|
||||
// This fills out toVerify, alreadyVerified (roots) and downloadedSignedTxns.
|
||||
fetchDependenciesAndCheckSignatures(txHashes, toVerify, alreadyVerified, downloadedSignedTxns)
|
||||
// For each transaction, verify it and insert it into the database. As we are iterating over them in a
|
||||
// depth-first order, we should not encounter any verification failures due to missing data. If we fail
|
||||
// half way through, it's no big deal, although it might result in us attempting to re-download data
|
||||
// redundantly next time we attempt verification.
|
||||
val result = ArrayList<LedgerTransaction>()
|
||||
|
||||
if (stx != null) {
|
||||
// Check the signatures on the stx first.
|
||||
toVerify += stx!!.verifyToLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
|
||||
} else if (wtx != null) {
|
||||
wtx!!.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
|
||||
for (tx in newTxns) {
|
||||
// Resolve to a LedgerTransaction and then run all contracts.
|
||||
val ltx = tx.toLedgerTransaction(serviceHub)
|
||||
ltx.verify()
|
||||
serviceHub.recordTransactions(tx)
|
||||
result += ltx
|
||||
}
|
||||
|
||||
// Run all the contracts and throw an exception if any of them reject.
|
||||
TransactionGroup(toVerify, alreadyVerified).verify()
|
||||
// If this protocol is resolving a specific transaction, make sure we have its attachments and then verify
|
||||
// it as well, but don't insert to the database. Note that when we were given a SignedTransaction (stx != null)
|
||||
// we *could* insert, because successful verification implies we have everything we need here, and it might
|
||||
// be a clearer API if we do that. But for consistency with the other c'tor we currently do not.
|
||||
stx?.let {
|
||||
fetchMissingAttachments(listOf(it.tx))
|
||||
val ltx = it.toLedgerTransaction(serviceHub)
|
||||
ltx.verify()
|
||||
result += ltx
|
||||
}
|
||||
wtx?.let {
|
||||
fetchMissingAttachments(listOf(it))
|
||||
val ltx = it.toLedgerTransaction(serviceHub)
|
||||
ltx.verify()
|
||||
result += ltx
|
||||
}
|
||||
|
||||
// Now write all the transactions we just validated back to the database for next time, including
|
||||
// signatures so we can serve up these transactions to other peers when we, in turn, send one that
|
||||
// depends on them onto another peer.
|
||||
//
|
||||
// It may seem tempting to write transactions to the database as we receive them, instead of all at once
|
||||
// here at the end. Doing it this way avoids cases where a transaction is in the database but its
|
||||
// dependencies aren't, or an unvalidated and possibly broken tx is there.
|
||||
serviceHub.recordTransactions(downloadedSignedTxns)
|
||||
return result
|
||||
}
|
||||
|
||||
override val topic: String get() = throw UnsupportedOperationException()
|
||||
|
||||
@Suspendable
|
||||
private fun fetchDependenciesAndCheckSignatures(depsToCheck: Set<SecureHash>,
|
||||
toVerify: HashSet<LedgerTransaction>,
|
||||
alreadyVerified: HashSet<LedgerTransaction>,
|
||||
downloadedSignedTxns: ArrayList<SignedTransaction>) {
|
||||
// Maintain a work queue of all hashes to load/download, initialised with our starting set.
|
||||
// Then either fetch them from the database or request them from the other side. Look up the
|
||||
// signatures against our identity database, filtering the transactions into 'already checked'
|
||||
// and 'need to check' sets.
|
||||
private fun downloadDependencies(depsToCheck: Set<SecureHash>): List<SignedTransaction> {
|
||||
// Maintain a work queue of all hashes to load/download, initialised with our starting set. Then do a breadth
|
||||
// first traversal across the dependency graph.
|
||||
//
|
||||
// TODO: This approach has two problems. Analyze and resolve them:
|
||||
//
|
||||
@ -103,45 +115,49 @@ class ResolveTransactionsProtocol(private val txHashes: Set<SecureHash>,
|
||||
|
||||
val nextRequests = LinkedHashSet<SecureHash>() // Keep things unique but ordered, for unit test stability.
|
||||
nextRequests.addAll(depsToCheck)
|
||||
val resultQ = LinkedHashMap<SecureHash, SignedTransaction>()
|
||||
|
||||
var limitCounter = 0
|
||||
while (nextRequests.isNotEmpty()) {
|
||||
val (fromDisk, downloads) = subProtocol(FetchTransactionsProtocol(nextRequests, otherSide))
|
||||
// Don't re-download the same tx when we haven't verified it yet but it's referenced multiple times in the
|
||||
// graph we're traversing.
|
||||
val notAlreadyFetched = nextRequests.filterNot { it in resultQ }.toSet()
|
||||
nextRequests.clear()
|
||||
|
||||
// TODO: This could be done in parallel with other fetches for extra speed.
|
||||
resolveMissingAttachments(downloads)
|
||||
if (notAlreadyFetched.isEmpty()) // Done early.
|
||||
break
|
||||
|
||||
// Resolve any legal identities from known public keys in the signatures.
|
||||
val downloadedTxns = downloads.map {
|
||||
it.verifyToLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
|
||||
}
|
||||
// Request the standalone transaction data (which may refer to things we don't yet have).
|
||||
val downloads: List<SignedTransaction> = subProtocol(FetchTransactionsProtocol(notAlreadyFetched, otherSide)).downloaded
|
||||
|
||||
// Do the same for transactions loaded from disk (i.e. we checked them previously).
|
||||
val loadedTxns = fromDisk.map {
|
||||
it.verifyToLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
|
||||
}
|
||||
fetchMissingAttachments(downloads.map { it.tx })
|
||||
|
||||
toVerify.addAll(downloadedTxns)
|
||||
alreadyVerified.addAll(loadedTxns)
|
||||
downloadedSignedTxns.addAll(downloads)
|
||||
for (stx in downloads)
|
||||
check(resultQ.putIfAbsent(stx.id, stx) == null) // Assert checks the filter at the start.
|
||||
|
||||
// And now add all the input states to the work queue for database or remote resolution.
|
||||
nextRequests.addAll(downloadedTxns.flatMap { it.inputs }.map { it.txhash })
|
||||
// Add all input states to the work queue.
|
||||
val inputHashes = downloads.flatMap { it.tx.inputs }.map { it.txhash }
|
||||
nextRequests.addAll(inputHashes)
|
||||
|
||||
// And loop around ...
|
||||
// TODO: Figure out a more appropriate DOS limit here, 5000 is simply a guess.
|
||||
// TODO: Figure out a more appropriate DOS limit here, 5000 is simply a very bad guess.
|
||||
// TODO: Unit test the DoS limit.
|
||||
limitCounter += nextRequests.size
|
||||
limitCounter = limitCounter checkedAdd nextRequests.size
|
||||
if (limitCounter > 5000)
|
||||
throw ExcessivelyLargeTransactionGraph()
|
||||
}
|
||||
|
||||
return resultQ.values.reversed()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all the dependencies of the given transactions, deepest first i.e. the last downloaded comes
|
||||
* first in the returned list and thus doesn't have any unverified dependencies.
|
||||
*/
|
||||
@Suspendable
|
||||
private fun resolveMissingAttachments(downloads: List<SignedTransaction>) {
|
||||
private fun fetchMissingAttachments(downloads: List<WireTransaction>) {
|
||||
// TODO: This could be done in parallel with other fetches for extra speed.
|
||||
val missingAttachments = downloads.flatMap { stx ->
|
||||
stx.tx.attachments.filter { serviceHub.storageService.attachments.openAttachment(it) == null }
|
||||
stx.attachments.filter { serviceHub.storageService.attachments.openAttachment(it) == null }
|
||||
}
|
||||
subProtocol(FetchAttachmentsProtocol(missingAttachments.toSet(), otherSide))
|
||||
}
|
||||
|
@ -98,21 +98,21 @@ object TwoPartyDealProtocol {
|
||||
fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData<SignedTransaction>): SignedTransaction {
|
||||
progressTracker.currentStep = VERIFYING
|
||||
|
||||
untrustedPartialTX.validate {
|
||||
untrustedPartialTX.validate { stx ->
|
||||
progressTracker.nextStep()
|
||||
|
||||
// Check that the tx proposed by the buyer is valid.
|
||||
val missingSigs = it.verify(throwIfSignaturesAreMissing = false)
|
||||
val missingSigs = stx.verifySignatures(throwIfSignaturesAreMissing = false)
|
||||
if (missingSigs != setOf(myKeyPair.public, notaryNode.identity.owningKey))
|
||||
throw SignatureException("The set of missing signatures is not as expected: $missingSigs")
|
||||
|
||||
val wtx: WireTransaction = it.tx
|
||||
logger.trace { "Received partially signed transaction: ${it.id}" }
|
||||
val wtx: WireTransaction = stx.tx
|
||||
logger.trace { "Received partially signed transaction: ${stx.id}" }
|
||||
|
||||
checkDependencies(it)
|
||||
checkDependencies(stx)
|
||||
|
||||
// This verifies that the transaction is contract-valid, even though it is missing signatures.
|
||||
serviceHub.verifyTransaction(wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments))
|
||||
wtx.toLedgerTransaction(serviceHub).verify()
|
||||
|
||||
// There are all sorts of funny games a malicious secondary might play here, we should fix them:
|
||||
//
|
||||
@ -124,7 +124,7 @@ object TwoPartyDealProtocol {
|
||||
// but the goal of this code is not to be fully secure (yet), but rather, just to find good ways to
|
||||
// express protocol state machines on top of the messaging layer.
|
||||
|
||||
return it
|
||||
return stx
|
||||
}
|
||||
}
|
||||
|
||||
@ -226,7 +226,7 @@ object TwoPartyDealProtocol {
|
||||
logger.trace { "Got signatures from other party, verifying ... " }
|
||||
|
||||
val fullySigned = stx + signatures.sellerSig + signatures.notarySig
|
||||
fullySigned.verify()
|
||||
fullySigned.verifySignatures()
|
||||
|
||||
logger.trace { "Signatures received are valid. Deal transaction complete! :-)" }
|
||||
|
||||
@ -471,4 +471,4 @@ object TwoPartyDealProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -23,11 +23,11 @@ class ValidatingNotaryProtocol(otherSide: Party,
|
||||
uniquenessProvider: UniquenessProvider) : NotaryProtocol.Service(otherSide, sessionIdForSend, sessionIdForReceive, timestampChecker, uniquenessProvider) {
|
||||
@Suspendable
|
||||
override fun beforeCommit(stx: SignedTransaction, reqIdentity: Party) {
|
||||
val wtx = stx.tx
|
||||
try {
|
||||
checkSignatures(stx)
|
||||
validateDependencies(reqIdentity, wtx)
|
||||
checkContractValid(wtx)
|
||||
val wtx = stx.tx
|
||||
resolveTransaction(reqIdentity, wtx)
|
||||
wtx.toLedgerTransaction(serviceHub).verify()
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is TransactionVerificationException,
|
||||
@ -39,18 +39,13 @@ class ValidatingNotaryProtocol(otherSide: Party,
|
||||
|
||||
private fun checkSignatures(stx: SignedTransaction) {
|
||||
val myKey = serviceHub.storageService.myLegalIdentity.owningKey
|
||||
val missing = stx.verify(false) - myKey
|
||||
val missing = stx.verifySignatures(throwIfSignaturesAreMissing = false) - myKey
|
||||
|
||||
if (missing.isNotEmpty()) throw NotaryException(NotaryError.SignaturesMissing(missing.toList()))
|
||||
}
|
||||
|
||||
private fun checkContractValid(wtx: WireTransaction) {
|
||||
val ltx = wtx.toLedgerTransaction(serviceHub.identityService, serviceHub.storageService.attachments)
|
||||
serviceHub.verifyTransaction(ltx)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun validateDependencies(reqIdentity: Party, wtx: WireTransaction) {
|
||||
private fun resolveTransaction(reqIdentity: Party, wtx: WireTransaction) {
|
||||
subProtocol(ResolveTransactionsProtocol(wtx, reqIdentity))
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ package com.r3corda.core.serialization
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.crypto.newSecureRandom
|
||||
import com.r3corda.core.node.services.testing.MockStorageService
|
||||
import com.r3corda.core.seconds
|
||||
import com.r3corda.core.testing.*
|
||||
import org.junit.Before
|
||||
@ -97,7 +96,7 @@ class TransactionSerializationTests {
|
||||
tx2.signWith(DUMMY_NOTARY_KEY)
|
||||
tx2.signWith(DUMMY_KEY_2)
|
||||
|
||||
signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verify()
|
||||
signedTX.copy(sigs = tx2.toSignedTransaction().sigs).verifySignatures()
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,10 +106,6 @@ class TransactionSerializationTests {
|
||||
tx.signWith(DUMMY_KEY_1)
|
||||
tx.signWith(DUMMY_NOTARY_KEY)
|
||||
val stx = tx.toSignedTransaction()
|
||||
val ltx = stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments)
|
||||
assertEquals(tx.commands().map { it.value }, ltx.commands.map { it.value })
|
||||
assertEquals(tx.inputStates(), ltx.inputs)
|
||||
assertEquals(tx.outputStates(), ltx.outputs)
|
||||
assertEquals(TEST_TX_TIME, ltx.commands.getTimestampBy(DUMMY_NOTARY)!!.midpoint)
|
||||
assertEquals(TEST_TX_TIME, (stx.tx.commands[1].value as TimestampCommand).midpoint)
|
||||
}
|
||||
}
|
||||
|
@ -3,15 +3,14 @@ package com.r3corda.contracts
|
||||
import com.r3corda.core.contracts.DOLLARS
|
||||
import com.r3corda.core.contracts.LedgerTransaction
|
||||
import com.r3corda.core.contracts.`issued by`
|
||||
import com.r3corda.core.contracts.verifyToLedgerTransaction
|
||||
import com.r3corda.core.node.services.testing.MockStorageService
|
||||
import com.r3corda.core.contracts.toLedgerTransaction
|
||||
import com.r3corda.core.node.services.testing.MockServices
|
||||
import com.r3corda.core.seconds
|
||||
import com.r3corda.core.serialization.OpaqueBytes
|
||||
import com.r3corda.core.testing.*
|
||||
import org.junit.Before
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
//import java.util.*
|
||||
//import kotlin.test.fail
|
||||
|
||||
/**
|
||||
* unit test cases that confirms the correct behavior of the AccountReceivable smart contract
|
||||
@ -29,6 +28,8 @@ class AccountReceivableTests {
|
||||
|
||||
val notary = DUMMY_NOTARY
|
||||
|
||||
lateinit var services: MockServices
|
||||
|
||||
val invoiceProperties = Invoice.InvoiceProperties(
|
||||
invoiceID = "123",
|
||||
seller = LocDataStructures.Company(
|
||||
@ -67,6 +68,11 @@ class AccountReceivableTests {
|
||||
PAST, FUTURE
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
services = MockServices()
|
||||
}
|
||||
|
||||
fun generateInvoiceIssueTxn(kind: WhatKind = WhatKind.FUTURE): LedgerTransaction {
|
||||
val genTX: LedgerTransaction = run {
|
||||
val pastProp = initialInvoiceState.props.copy(invoiceDate =
|
||||
@ -83,8 +89,9 @@ class AccountReceivableTests {
|
||||
signWith(MINI_CORP_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
gtx.toSignedTransaction().verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments)
|
||||
gtx.toSignedTransaction().toLedgerTransaction(services)
|
||||
}
|
||||
genTX.verify()
|
||||
return genTX
|
||||
}
|
||||
|
||||
|
@ -2,8 +2,9 @@ package com.r3corda.contracts
|
||||
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.node.services.testing.MockStorageService
|
||||
import com.r3corda.core.node.services.testing.MockServices
|
||||
import com.r3corda.core.testing.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
@ -42,7 +43,12 @@ class BillOfLadingAgreementTests {
|
||||
props =pros
|
||||
)
|
||||
|
||||
val attachments = MockStorageService().attachments
|
||||
lateinit var services: MockServices
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
services = MockServices()
|
||||
}
|
||||
|
||||
//Generation method tests
|
||||
|
||||
@ -52,15 +58,14 @@ class BillOfLadingAgreementTests {
|
||||
signWith(ALICE_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments)
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun issueGenerationMethod_Unsigned() {
|
||||
val ptx = BillOfLadingAgreement().generateIssue(Bill.owner, Bill.beneficiary, Bill.props, DUMMY_NOTARY)
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments)
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
@ -68,9 +73,7 @@ class BillOfLadingAgreementTests {
|
||||
val ptx = BillOfLadingAgreement().generateIssue(Bill.owner, Bill.beneficiary, Bill.props, DUMMY_NOTARY).apply {
|
||||
signWith(BOB_KEY)
|
||||
}
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments)
|
||||
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
// @Test // TODO: Fix Test
|
||||
@ -85,8 +88,7 @@ class BillOfLadingAgreementTests {
|
||||
ptx.signWith(MEGA_CORP_KEY) //Signed by owner
|
||||
ptx.signWith(BOB_KEY) //and beneficiary
|
||||
// ptx.signWith(CHARLIE_KEY) // ??????
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments)
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
@ -98,8 +100,7 @@ class BillOfLadingAgreementTests {
|
||||
)
|
||||
BillOfLadingAgreement().generateTransferAndEndorse(ptx,sr,CHARLIE_PUBKEY, CHARLIE)
|
||||
ptx.signWith(MEGA_CORP_KEY) //Signed by owner
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments)
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
@ -111,8 +112,7 @@ class BillOfLadingAgreementTests {
|
||||
)
|
||||
BillOfLadingAgreement().generateTransferAndEndorse(ptx,sr,CHARLIE_PUBKEY, CHARLIE)
|
||||
ptx.signWith(BOB_KEY) //Signed by beneficiary
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments)
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
// @Test // TODO Fix Test
|
||||
@ -124,8 +124,7 @@ class BillOfLadingAgreementTests {
|
||||
)
|
||||
BillOfLadingAgreement().generateTransferPossession(ptx,sr,CHARLIE_PUBKEY)
|
||||
ptx.signWith(MEGA_CORP_KEY) //Signed by owner
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments)
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
@ -136,8 +135,7 @@ class BillOfLadingAgreementTests {
|
||||
StateRef(SecureHash.randomSHA256(), Random().nextInt(32))
|
||||
)
|
||||
BillOfLadingAgreement().generateTransferPossession(ptx,sr,CHARLIE_PUBKEY)
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE,attachments)
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
//Custom transaction tests
|
||||
|
@ -3,8 +3,10 @@ package com.r3corda.contracts
|
||||
import com.r3corda.contracts.asset.Cash
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.node.services.testing.MockServices
|
||||
import com.r3corda.core.serialization.OpaqueBytes
|
||||
import com.r3corda.core.testing.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
@ -114,23 +116,26 @@ class LOCTests {
|
||||
)
|
||||
)
|
||||
|
||||
lateinit var services: MockServices
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
services = MockServices()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun issueSignedByBank() {
|
||||
val ptx = LOC().generateIssue(LOCstate.beneficiaryPaid, LOCstate.issued, LOCstate.terminated, LOCstate.props, DUMMY_NOTARY).apply {
|
||||
val ptx = LOC().generateIssue(LOCstate.beneficiaryPaid, true, LOCstate.terminated, LOCstate.props, DUMMY_NOTARY).apply {
|
||||
signWith(MEGA_CORP_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
}
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verify()
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun issueUnsigned() {
|
||||
val ptx = LOC().generateIssue(LOCstate.beneficiaryPaid, LOCstate.issued, LOCstate.terminated, LOCstate.props, DUMMY_NOTARY)
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verify()
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
@ -138,9 +143,7 @@ class LOCTests {
|
||||
val ptx = LOC().generateIssue(LOCstate.beneficiaryPaid, LOCstate.issued, LOCstate.terminated, LOCstate.props, DUMMY_NOTARY).apply {
|
||||
signWith(BOB_KEY)
|
||||
}
|
||||
val stx = ptx.toSignedTransaction()
|
||||
stx.verify()
|
||||
|
||||
ptx.toSignedTransaction().toLedgerTransaction(services).verify()
|
||||
}
|
||||
|
||||
|
||||
|
@ -257,11 +257,10 @@ class TwoPartyTradeProtocolTests {
|
||||
}
|
||||
val attachmentID = attachment(ByteArrayInputStream(stream.toByteArray()))
|
||||
|
||||
val issuer = MEGA_CORP.ref(1)
|
||||
val bobsFakeCash = fillUpForBuyer(false, bobNode.keyManagement.freshKey().public, issuer).second
|
||||
val bobsFakeCash = fillUpForBuyer(false, bobNode.keyManagement.freshKey().public).second
|
||||
val bobsSignedTxns = insertFakeTransactions(bobsFakeCash, bobNode.services)
|
||||
val alicesFakePaper = fillUpForSeller(false, aliceNode.storage.myLegalIdentity.owningKey,
|
||||
1200.DOLLARS `issued by` issuer, notaryNode.info.identity, attachmentID).second
|
||||
1200.DOLLARS `issued by` DUMMY_CASH_ISSUER, notaryNode.info.identity, attachmentID).second
|
||||
val alicesSignedTxns = insertFakeTransactions(alicesFakePaper, aliceNode.services, aliceNode.storage.myLegalIdentityKey)
|
||||
|
||||
val buyerSessionID = random63BitValue()
|
||||
@ -326,9 +325,11 @@ class TwoPartyTradeProtocolTests {
|
||||
TxRecord.Get(bobsFakeCash[0].id),
|
||||
// Bob answers with the transactions that are now all verifiable, as Alice bottomed out.
|
||||
// Bob's transactions are valid, so she commits to the database
|
||||
TxRecord.Add(bobsSignedTxns[bobsFakeCash[1].id]!!),
|
||||
TxRecord.Add(bobsSignedTxns[bobsFakeCash[2].id]!!),
|
||||
TxRecord.Add(bobsSignedTxns[bobsFakeCash[0].id]!!),
|
||||
TxRecord.Get(bobsFakeCash[0].id), // Verify
|
||||
TxRecord.Add(bobsSignedTxns[bobsFakeCash[2].id]!!),
|
||||
TxRecord.Get(bobsFakeCash[0].id), // Verify
|
||||
TxRecord.Add(bobsSignedTxns[bobsFakeCash[1].id]!!),
|
||||
// Now she verifies the transaction is contract-valid (not signature valid) which means
|
||||
// looking up the states again.
|
||||
TxRecord.Get(bobsFakeCash[1].id),
|
||||
@ -413,7 +414,7 @@ class TwoPartyTradeProtocolTests {
|
||||
wtxToSign: List<WireTransaction>,
|
||||
services: ServiceHub,
|
||||
vararg extraKeys: KeyPair): Map<SecureHash, SignedTransaction> {
|
||||
val signed: List<SignedTransaction> = signAll(wtxToSign, extraKeys.toList())
|
||||
val signed: List<SignedTransaction> = signAll(wtxToSign, extraKeys.toList() + DUMMY_CASH_ISSUER_KEY)
|
||||
services.recordTransactions(signed)
|
||||
val validatedTransactions = services.storageService.validatedTransactions
|
||||
if (validatedTransactions is RecordingTransactionStorage) {
|
||||
@ -425,16 +426,15 @@ class TwoPartyTradeProtocolTests {
|
||||
private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.fillUpForBuyer(
|
||||
withError: Boolean,
|
||||
owner: PublicKey = BOB_PUBKEY,
|
||||
issuer: PartyAndReference = MEGA_CORP.ref(1)): Pair<Wallet, List<WireTransaction>> {
|
||||
issuer: PartyAndReference = DUMMY_CASH_ISSUER): Pair<Wallet, List<WireTransaction>> {
|
||||
// Bob (Buyer) has some cash he got from the Bank of Elbonia, Alice (Seller) has some commercial paper she
|
||||
// wants to sell to Bob.
|
||||
|
||||
val eb1 = transaction {
|
||||
// Issued money to itself.
|
||||
output("elbonian money 1") { 800.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY }
|
||||
output("elbonian money 2") { 1000.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY }
|
||||
if (!withError)
|
||||
command(MEGA_CORP_PUBKEY) { Cash.Commands.Issue() }
|
||||
command(DUMMY_CASH_ISSUER_KEY.public) { Cash.Commands.Issue() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
if (withError) {
|
||||
this.fails()
|
||||
|
@ -7,7 +7,6 @@ import com.r3corda.contracts.testing.fillWithSomeTestCash
|
||||
import com.r3corda.core.contracts.*
|
||||
import com.r3corda.core.crypto.SecureHash
|
||||
import com.r3corda.core.node.services.WalletService
|
||||
import com.r3corda.core.node.services.testing.MockStorageService
|
||||
import com.r3corda.core.node.services.testing.MockServices
|
||||
import com.r3corda.core.testing.*
|
||||
import com.r3corda.core.utilities.BriefLogFormatter
|
||||
@ -70,7 +69,7 @@ class WalletWithCashTest {
|
||||
Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), freshKey.public, DUMMY_NOTARY)
|
||||
signWith(MEGA_CORP_KEY)
|
||||
}.toSignedTransaction()
|
||||
val myOutput = usefulTX.verifyToLedgerTransaction(MOCK_IDENTITY_SERVICE, MockStorageService().attachments).outRef<Cash.State>(0)
|
||||
val myOutput = usefulTX.toLedgerTransaction(services).outRef<Cash.State>(0)
|
||||
|
||||
// A tx that spends our money.
|
||||
val spendTX = TransactionType.General.Builder().apply {
|
||||
|
Loading…
Reference in New Issue
Block a user