Pronoy Chaudhuri
Pronoy Chaudhuri

Reputation: 131

Why do I get initiator of CollectSignaturesFlow must pass in exact sessions error in Corda?

I am making a CorDapp that involves two parties - a Client and an Underwriter. I have two main flows, one being IssuePolicy and another being PayoutPolicy. When I run each flow once, there are no issues. When I run IssuePolicy again, I receive the error: The Initiator of CollectSignaturesFlow must pass in exactly the sessions required to sign the transaction. I've already searched Stack Overflow and found this post: Flow Exception in CollectSignaturesFlow but I believe I have signed with one party and initiated flow with the other, so I am not sure whether this solution applies to me. Any help would be appreciated!

IssuePolicy Flow:

        // Step 3. Building.
        progressTracker.currentStep = BUILDING
        val notary = serviceHub.networkMapCache.notaryIdentities[0]
        val utx = TransactionBuilder(notary = notary)
                .addOutputState(policy, INSUREFLIGHT_CONTRACT_ID)
                .addCommand(InsureFlightContract.Commands.Issue(), policy.participants.map { it.owningKey })
                .setTimeWindow(serviceHub.clock.instant(), 30.seconds)

        // Stage 4. Get some cash from the vault and add a spend to our transaction builder.
        // We pay cash to the underwriter's policy key.
        val (_, cashSigningKeys) = Cash.generateSpend(serviceHub, utx, premium, underwriter)
        check(cashSigningKeys == cashSigningKeys){
            throw FlowException("")
        }
        // Step 5. Sign the transaction.
        progressTracker.currentStep = SIGNING
        val ptx = serviceHub.signInitialTransaction(utx, policy.client.owningKey)

        // Step 6. Get the counter-party signature.
        progressTracker.currentStep = COLLECTING
        val otherpartySession = initiateFlow(underwriter)
        val stx = subFlow(CollectSignaturesFlow(
                ptx,
                listOf(otherpartySession),
                COLLECTING.childProgressTracker())
        )

        // Step 7. Finalize the transaction.
        progressTracker.currentStep = FINALIZING
        return subFlow(FinalityFlow(stx, FINALIZING.childProgressTracker()))
    }
}

// Allows counterparty to respond.
@InitiatedBy(Initiator::class)
class IssuePolicyResponder(val otherPartySession: FlowSession) : FlowLogic<Unit>() {
@Suspendable
override fun call() {
    val signTransactionFlow = object : SignTransactionFlow(otherPartySession, SignTransactionFlow.tracker()) {
        override fun checkTransaction(stx: SignedTransaction) = requireThat {
        }
    }

    subFlow(signTransactionFlow)
}
}
}

Policy State Definition:

//Policy Class, includes premium, claim, client, underwriter, flight, and policyID
data class Policy(val premium: Amount<Currency>,
              val claim: Amount<Currency>,
              val client: Party,
              val underwriter: Party,
              val flight: String,
              override val linearId: UniqueIdentifier = UniqueIdentifier()) : LinearState {

//Get clients and underwriters
override val participants: List<Party> get() = listOf(client, underwriter)

//Functions to update policy parameters
fun payPremium(amountToPay: Amount<Currency>) = copy(premium = premium + amountToPay)
fun payClaim(amountToPay: Amount<Currency>) = copy(claim = claim + amountToPay)
fun withNewClient(newClient: Party) = copy(client = newClient)
fun withNewUnderwriter(newUnderwriter: Party) = copy(underwriter = newUnderwriter)
fun withNewFlight(newFlight: String) = copy(flight = newFlight)

//Provides response
override fun toString(): String {
    val clientString = (client as? Party)?.name?.organisation ?: client.owningKey.toBase58String()
    val underwriterString = (underwriter as? Party)?.name?.organisation ?: underwriter.owningKey.toBase58String()
    return "Policy($linearId): $clientString has paid a premium of $$premium for flight $flight, underwritten by $underwriterString with" +
            "a claim amount of $$claim."
}
}

Commands in the Contract:

interface Commands : CommandData {
    class Issue : TypeOnlyCommandData(), Commands
    class Settle : TypeOnlyCommandData(), Commands
}

override fun verify(tx: LedgerTransaction): Unit {
    val command = tx.commands.requireSingleCommand<Commands>()
    val setOfSigners = command.signers.toSet()
    when (command.value) {
        is Commands.Issue -> verifyIssue(tx, setOfSigners)
        is Commands.Settle -> verifySettle(tx, setOfSigners)
        else -> throw IllegalArgumentException("Unrecognized command. You can only issue or settle.")
    }
}

Upvotes: 2

Views: 857

Answers (1)

Joel
Joel

Reputation: 23140

Not all the cash in your node's vault will be owned by your node's main public key. This is because when Cash.generateSpend generates change outputs, this change is assigned to a new confidential identity instead of your node's main identity for privacy reasons.

This is the purpose of the cashSigningKeys returned by Cash.generateSpend. It's a list of the public keys that own all the cash added to the transaction builder by Cash.generateSpend.

I'm guessing that the first time you run IssuePolicy/PayoutPolicy, it generates some cash owned by a new confidential identity. You never sign the transaction with the key of the new confidential identity. Effectively, it is this confidential identity's session that is missing when you call CollectSignaturesFlow.

Of course, it doesn't make sense to create a session with this confidential identity, since it actually corresponds to the node running the flow. Instead, you need to take the cashSigningKeys returned by Cash.generateSpend, and sign the transaction with these as well before calling CollectSignaturesFlow, by calling:

val ptx = serviceHub.signInitialTransaction(
    utx, 
    cashSigningKeys + policy.client.owningKey)

Upvotes: 3

Related Questions