Remove dummy cash issuer from two party trade flows

Switch to using a dedicated bank node in two party trade flows so that when KYC checks are enforced, all nodes on the network know who the issuer is.
This commit is contained in:
Ross Nicoll 2017-07-19 16:00:14 +01:00 committed by GitHub
parent 070f0c040e
commit 9d3ad5fe06

View File

@ -2,27 +2,33 @@ package net.corda.node.messaging
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.CommercialPaper import net.corda.contracts.CommercialPaper
import net.corda.contracts.asset.* import net.corda.contracts.asset.CASH
import net.corda.core.* import net.corda.contracts.asset.Cash
import net.corda.contracts.asset.`issued by`
import net.corda.contracts.asset.`owned by`
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sign
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StateMachineRunId import net.corda.core.flows.StateMachineRunId
import net.corda.core.getOrThrow
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.AnonymousPartyAndPath
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.FlowStateMachine
import net.corda.core.map
import net.corda.core.messaging.DataFeed import net.corda.core.messaging.DataFeed
import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.messaging.StateMachineTransactionMapping import net.corda.core.messaging.StateMachineTransactionMapping
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
import net.corda.core.rootCause
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.toFuture
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
@ -51,7 +57,6 @@ import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.math.BigInteger import java.math.BigInteger
import java.security.KeyPair import java.security.KeyPair
import java.security.PublicKey
import java.util.* import java.util.*
import java.util.concurrent.Future import java.util.concurrent.Future
import java.util.jar.JarOutputStream import java.util.jar.JarOutputStream
@ -89,10 +94,12 @@ class TwoPartyTradeFlowTests {
mockNet = MockNetwork(false, true) mockNet = MockNetwork(false, true)
ledger { ledger {
val basketOfNodes = mockNet.createSomeNodes(2) val basketOfNodes = mockNet.createSomeNodes(3)
val notaryNode = basketOfNodes.notaryNode val notaryNode = basketOfNodes.notaryNode
val aliceNode = basketOfNodes.partyNodes[0] val aliceNode = basketOfNodes.partyNodes[0]
val bobNode = basketOfNodes.partyNodes[1] val bobNode = basketOfNodes.partyNodes[1]
val bankNode = basketOfNodes.partyNodes[2]
val issuer = bankNode.info.legalIdentity.ref(1, 2, 3)
aliceNode.disableDBCloseOnStop() aliceNode.disableDBCloseOnStop()
bobNode.disableDBCloseOnStop() bobNode.disableDBCloseOnStop()
@ -102,11 +109,11 @@ class TwoPartyTradeFlowTests {
} }
val alicesFakePaper = aliceNode.database.transaction { val alicesFakePaper = aliceNode.database.transaction {
fillUpForSeller(false, aliceNode.info.legalIdentity, fillUpForSeller(false, issuer, aliceNode.info.legalIdentity,
1200.DOLLARS `issued by` DUMMY_CASH_ISSUER, null, notaryNode.info.notaryIdentity).second 1200.DOLLARS `issued by` bankNode.info.legalIdentity.ref(0), null, notaryNode.info.notaryIdentity).second
} }
insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, MEGA_CORP_PUBKEY) insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
val (bobStateMachine, aliceResult) = runBuyerAndSeller(notaryNode, aliceNode, bobNode, val (bobStateMachine, aliceResult) = runBuyerAndSeller(notaryNode, aliceNode, bobNode,
"alice's paper".outputStateAndRef()) "alice's paper".outputStateAndRef())
@ -137,6 +144,8 @@ class TwoPartyTradeFlowTests {
val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name)
val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name)
val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name)
val cpIssuer = bankNode.info.legalIdentity.ref(1, 2, 3)
aliceNode.disableDBCloseOnStop() aliceNode.disableDBCloseOnStop()
bobNode.disableDBCloseOnStop() bobNode.disableDBCloseOnStop()
@ -146,11 +155,11 @@ class TwoPartyTradeFlowTests {
} }
val alicesFakePaper = aliceNode.database.transaction { val alicesFakePaper = aliceNode.database.transaction {
fillUpForSeller(false, aliceNode.info.legalIdentity, fillUpForSeller(false, cpIssuer, aliceNode.info.legalIdentity,
1200.DOLLARS `issued by` DUMMY_CASH_ISSUER, null, notaryNode.info.notaryIdentity).second 1200.DOLLARS `issued by` bankNode.info.legalIdentity.ref(0), null, notaryNode.info.notaryIdentity).second
} }
insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, MEGA_CORP_PUBKEY) insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
val cashLockId = UUID.randomUUID() val cashLockId = UUID.randomUUID()
bobNode.database.transaction { bobNode.database.transaction {
@ -186,6 +195,8 @@ class TwoPartyTradeFlowTests {
val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name)
var bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) var bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name)
val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name)
val cpIssuer = bankNode.info.legalIdentity.ref(1, 2, 3)
aliceNode.services.identityService.registerIdentity(bobNode.info.legalIdentityAndCert) aliceNode.services.identityService.registerIdentity(bobNode.info.legalIdentityAndCert)
bobNode.services.identityService.registerIdentity(aliceNode.info.legalIdentityAndCert) bobNode.services.identityService.registerIdentity(aliceNode.info.legalIdentityAndCert)
@ -201,10 +212,10 @@ class TwoPartyTradeFlowTests {
bobNode.services.fillWithSomeTestCash(2000.DOLLARS, outputNotary = notaryNode.info.notaryIdentity) bobNode.services.fillWithSomeTestCash(2000.DOLLARS, outputNotary = notaryNode.info.notaryIdentity)
} }
val alicesFakePaper = aliceNode.database.transaction { val alicesFakePaper = aliceNode.database.transaction {
fillUpForSeller(false, aliceNode.info.legalIdentity, fillUpForSeller(false, cpIssuer, aliceNode.info.legalIdentity,
1200.DOLLARS `issued by` DUMMY_CASH_ISSUER, null, notaryNode.info.notaryIdentity).second 1200.DOLLARS `issued by` bankNode.info.legalIdentity.ref(0), null, notaryNode.info.notaryIdentity).second
} }
insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, MEGA_CORP_PUBKEY) insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
val aliceFuture = runBuyerAndSeller(notaryNode, aliceNode, bobNode, "alice's paper".outputStateAndRef()).sellerResult val aliceFuture = runBuyerAndSeller(notaryNode, aliceNode, bobNode, "alice's paper".outputStateAndRef()).sellerResult
// Everything is on this thread so we can now step through the flow one step at a time. // Everything is on this thread so we can now step through the flow one step at a time.
@ -305,6 +316,8 @@ class TwoPartyTradeFlowTests {
val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
val aliceNode = makeNodeWithTracking(notaryNode.network.myAddress, ALICE.name) val aliceNode = makeNodeWithTracking(notaryNode.network.myAddress, ALICE.name)
val bobNode = makeNodeWithTracking(notaryNode.network.myAddress, BOB.name) val bobNode = makeNodeWithTracking(notaryNode.network.myAddress, BOB.name)
val bankNode = makeNodeWithTracking(notaryNode.network.myAddress, BOC.name)
val issuer = bankNode.info.legalIdentity.ref(1, 2, 3)
ledger(aliceNode.services) { ledger(aliceNode.services) {
@ -319,16 +332,14 @@ class TwoPartyTradeFlowTests {
attachment(ByteArrayInputStream(stream.toByteArray())) attachment(ByteArrayInputStream(stream.toByteArray()))
} }
val extraKey = bobNode.services.keyManagementService.keys.single() val bobsFakeCash = fillUpForBuyer(false, issuer, AnonymousParty(bobNode.info.legalIdentity.owningKey),
val bobsFakeCash = fillUpForBuyer(false, AnonymousParty(extraKey),
DUMMY_CASH_ISSUER.party,
notaryNode.info.notaryIdentity).second notaryNode.info.notaryIdentity).second
val bobsSignedTxns = insertFakeTransactions(bobsFakeCash, bobNode, notaryNode, extraKey, DUMMY_CASH_ISSUER_KEY.public, MEGA_CORP_PUBKEY) val bobsSignedTxns = insertFakeTransactions(bobsFakeCash, bobNode, notaryNode, bankNode)
val alicesFakePaper = aliceNode.database.transaction { val alicesFakePaper = aliceNode.database.transaction {
fillUpForSeller(false, aliceNode.info.legalIdentity, fillUpForSeller(false, issuer, aliceNode.info.legalIdentity,
1200.DOLLARS `issued by` DUMMY_CASH_ISSUER, attachmentID, notaryNode.info.notaryIdentity).second 1200.DOLLARS `issued by` bankNode.info.legalIdentity.ref(0), attachmentID, notaryNode.info.notaryIdentity).second
} }
val alicesSignedTxns = insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, MEGA_CORP_PUBKEY) val alicesSignedTxns = insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
mockNet.runNetwork() // Clear network map registration messages mockNet.runNetwork() // Clear network map registration messages
@ -404,6 +415,8 @@ class TwoPartyTradeFlowTests {
val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
val aliceNode = makeNodeWithTracking(notaryNode.network.myAddress, ALICE.name) val aliceNode = makeNodeWithTracking(notaryNode.network.myAddress, ALICE.name)
val bobNode = makeNodeWithTracking(notaryNode.network.myAddress, BOB.name) val bobNode = makeNodeWithTracking(notaryNode.network.myAddress, BOB.name)
val bankNode = makeNodeWithTracking(notaryNode.network.myAddress, BOC.name)
val issuer = bankNode.info.legalIdentity.ref(1, 2, 3)
ledger(aliceNode.services) { ledger(aliceNode.services) {
@ -419,17 +432,16 @@ class TwoPartyTradeFlowTests {
} }
val bobsKey = bobNode.services.keyManagementService.keys.single() val bobsKey = bobNode.services.keyManagementService.keys.single()
val bobsFakeCash = fillUpForBuyer(false, AnonymousParty(bobsKey), val bobsFakeCash = fillUpForBuyer(false, issuer, AnonymousParty(bobsKey),
DUMMY_CASH_ISSUER.party,
notaryNode.info.notaryIdentity).second notaryNode.info.notaryIdentity).second
insertFakeTransactions(bobsFakeCash, bobNode, notaryNode, DUMMY_CASH_ISSUER_KEY.public, MEGA_CORP_PUBKEY) insertFakeTransactions(bobsFakeCash, bobNode, notaryNode, bankNode)
val alicesFakePaper = aliceNode.database.transaction { val alicesFakePaper = aliceNode.database.transaction {
fillUpForSeller(false, aliceNode.info.legalIdentity, fillUpForSeller(false, issuer, aliceNode.info.legalIdentity,
1200.DOLLARS `issued by` DUMMY_CASH_ISSUER, attachmentID, notaryNode.info.notaryIdentity).second 1200.DOLLARS `issued by` bankNode.info.legalIdentity.ref(0), attachmentID, notaryNode.info.notaryIdentity).second
} }
insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, MEGA_CORP_PUBKEY) insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
mockNet.runNetwork() // Clear network map registration messages mockNet.runNetwork() // Clear network map registration messages
@ -503,7 +515,7 @@ class TwoPartyTradeFlowTests {
} }
val buyerFlows: Observable<BuyerAcceptor> = buyerNode.registerInitiatedFlow(BuyerAcceptor::class.java) val buyerFlows: Observable<BuyerAcceptor> = buyerNode.registerInitiatedFlow(BuyerAcceptor::class.java)
val firstBuyerFiber = buyerFlows.toFuture().map { it.stateMachine } val firstBuyerFiber = buyerFlows.toFuture().map { it.stateMachine }
val seller = SellerInitiator(buyerNode.info.legalIdentity, notaryNode.info, assetToSell, 1000.DOLLARS, anonymousSeller.party) val seller = SellerInitiator(buyerNode.info.legalIdentity, notaryNode.info, assetToSell, 1000.DOLLARS, anonymousSeller)
val sellerResult = sellerNode.services.startFlow(seller).resultFuture val sellerResult = sellerNode.services.startFlow(seller).resultFuture
return RunResult(firstBuyerFiber, sellerResult, seller.stateMachine.id) return RunResult(firstBuyerFiber, sellerResult, seller.stateMachine.id)
} }
@ -513,7 +525,7 @@ class TwoPartyTradeFlowTests {
val notary: NodeInfo, val notary: NodeInfo,
val assetToSell: StateAndRef<OwnableState>, val assetToSell: StateAndRef<OwnableState>,
val price: Amount<Currency>, val price: Amount<Currency>,
val me: AbstractParty) : FlowLogic<SignedTransaction>() { val me: AnonymousPartyAndPath) : FlowLogic<SignedTransaction>() {
@Suspendable @Suspendable
override fun call(): SignedTransaction { override fun call(): SignedTransaction {
send(buyer, Pair(notary.notaryIdentity, price)) send(buyer, Pair(notary.notaryIdentity, price))
@ -522,7 +534,7 @@ class TwoPartyTradeFlowTests {
notary, notary,
assetToSell, assetToSell,
price, price,
me)) me.party))
} }
} }
@ -546,19 +558,20 @@ class TwoPartyTradeFlowTests {
val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name)
val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name) val aliceNode = mockNet.createPartyNode(notaryNode.network.myAddress, ALICE.name)
val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name) val bobNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOB.name)
val issuer = MEGA_CORP.ref(1, 2, 3) val bankNode = mockNet.createPartyNode(notaryNode.network.myAddress, BOC.name)
val issuer = bankNode.info.legalIdentity.ref(1, 2, 3)
val bobsBadCash = bobNode.database.transaction { val bobsBadCash = bobNode.database.transaction {
fillUpForBuyer(bobError, bobNode.info.legalIdentity, DUMMY_CASH_ISSUER.party, fillUpForBuyer(bobError, issuer, bobNode.info.legalIdentity,
notaryNode.info.notaryIdentity).second notaryNode.info.notaryIdentity).second
} }
val alicesFakePaper = aliceNode.database.transaction { val alicesFakePaper = aliceNode.database.transaction {
fillUpForSeller(aliceError, aliceNode.info.legalIdentity, fillUpForSeller(aliceError, issuer, aliceNode.info.legalIdentity,
1200.DOLLARS `issued by` issuer, null, notaryNode.info.notaryIdentity).second 1200.DOLLARS `issued by` issuer, null, notaryNode.info.notaryIdentity).second
} }
insertFakeTransactions(bobsBadCash, bobNode, notaryNode, DUMMY_CASH_ISSUER_KEY.public, MEGA_CORP_PUBKEY) insertFakeTransactions(bobsBadCash, bobNode, notaryNode, bankNode)
insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, MEGA_CORP_PUBKEY) insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, bankNode)
mockNet.runNetwork() // Clear network map registration messages mockNet.runNetwork() // Clear network map registration messages
@ -582,8 +595,8 @@ class TwoPartyTradeFlowTests {
private fun insertFakeTransactions( private fun insertFakeTransactions(
wtxToSign: List<WireTransaction>, wtxToSign: List<WireTransaction>,
node: AbstractNode, node: AbstractNode,
notaryNode: MockNetwork.MockNode, notaryNode: AbstractNode,
vararg extraKeys: PublicKey): Map<SecureHash, SignedTransaction> { vararg extraSigningNodes: AbstractNode): Map<SecureHash, SignedTransaction> {
val signed = wtxToSign.map { val signed = wtxToSign.map {
val bits = it.serialize() val bits = it.serialize()
@ -591,14 +604,8 @@ class TwoPartyTradeFlowTests {
val sigs = mutableListOf<DigitalSignature.WithKey>() val sigs = mutableListOf<DigitalSignature.WithKey>()
sigs.add(node.services.keyManagementService.sign(id.bytes, node.services.legalIdentityKey)) sigs.add(node.services.keyManagementService.sign(id.bytes, node.services.legalIdentityKey))
sigs.add(notaryNode.services.keyManagementService.sign(id.bytes, notaryNode.services.notaryIdentityKey)) sigs.add(notaryNode.services.keyManagementService.sign(id.bytes, notaryNode.services.notaryIdentityKey))
for (extraKey in extraKeys) { extraSigningNodes.forEach { currentNode ->
if (extraKey == DUMMY_CASH_ISSUER_KEY.public) { sigs.add(currentNode.services.keyManagementService.sign(id.bytes, currentNode.info.legalIdentity.owningKey))
sigs.add(DUMMY_CASH_ISSUER_KEY.sign(id.bytes))
} else if (extraKey == MEGA_CORP_PUBKEY) {
sigs.add(MEGA_CORP_KEY.sign(id.bytes))
} else {
sigs.add(node.services.keyManagementService.sign(id.bytes, extraKey))
}
} }
SignedTransaction(bits, sigs) SignedTransaction(bits, sigs)
} }
@ -614,10 +621,10 @@ class TwoPartyTradeFlowTests {
private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.fillUpForBuyer( private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.fillUpForBuyer(
withError: Boolean, withError: Boolean,
issuer: PartyAndReference,
owner: AbstractParty, owner: AbstractParty,
issuer: AbstractParty,
notary: Party): Pair<Vault<ContractState>, List<WireTransaction>> { notary: Party): Pair<Vault<ContractState>, List<WireTransaction>> {
val interimOwner = MEGA_CORP val interimOwner = issuer.party
// 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(transactionBuilder = TransactionBuilder(notary = notary)) { val eb1 = transaction(transactionBuilder = TransactionBuilder(notary = notary)) {
@ -625,10 +632,10 @@ class TwoPartyTradeFlowTests {
output("elbonian money 1", notary = notary) { 800.DOLLARS.CASH `issued by` issuer `owned by` interimOwner } output("elbonian money 1", notary = notary) { 800.DOLLARS.CASH `issued by` issuer `owned by` interimOwner }
output("elbonian money 2", notary = notary) { 1000.DOLLARS.CASH `issued by` issuer `owned by` interimOwner } output("elbonian money 2", notary = notary) { 1000.DOLLARS.CASH `issued by` issuer `owned by` interimOwner }
if (!withError) { if (!withError) {
command(issuer.owningKey) { Cash.Commands.Issue() } command(issuer.party.owningKey) { Cash.Commands.Issue() }
} else { } else {
// Put a broken command on so at least a signature is created // Put a broken command on so at least a signature is created
command(issuer.owningKey) { Cash.Commands.Move() } command(issuer.party.owningKey) { Cash.Commands.Move() }
} }
timeWindow(TEST_TX_TIME) timeWindow(TEST_TX_TIME)
if (withError) { if (withError) {
@ -660,15 +667,16 @@ class TwoPartyTradeFlowTests {
private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.fillUpForSeller( private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.fillUpForSeller(
withError: Boolean, withError: Boolean,
issuer: PartyAndReference,
owner: AbstractParty, owner: AbstractParty,
amount: Amount<Issued<Currency>>, amount: Amount<Issued<Currency>>,
attachmentID: SecureHash?, attachmentID: SecureHash?,
notary: Party): Pair<Vault<ContractState>, List<WireTransaction>> { notary: Party): Pair<Vault<ContractState>, List<WireTransaction>> {
val ap = transaction(transactionBuilder = TransactionBuilder(notary = notary)) { val ap = transaction(transactionBuilder = TransactionBuilder(notary = notary)) {
output("alice's paper", notary = notary) { output("alice's paper", notary = notary) {
CommercialPaper.State(MEGA_CORP.ref(1, 2, 3), owner, amount, TEST_TX_TIME + 7.days) CommercialPaper.State(issuer, owner, amount, TEST_TX_TIME + 7.days)
} }
command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } command(issuer.party.owningKey) { CommercialPaper.Commands.Issue() }
if (!withError) if (!withError)
timeWindow(time = TEST_TX_TIME) timeWindow(time = TEST_TX_TIME)
if (attachmentID != null) if (attachmentID != null)