0
votes

I am using Corda's Account library and currently unable to transfer a state from one owner (Account) to another (Account) hosted on separate nodes. Getting different errors at different time. But I am pretty sure it has to do with collecting signatures.

Is it ok to collect signature from owner account (anonymous party) or does it have to be the nodes owning key. Also, is it mandatory to collect signatures from all parties involved in the participants list in states class. For instance in the below example, is it necessary to collect signature from issue during a transfer flow.

An Issuer issues a state to an owner using Registration Flow. An owner can transfer to another owner using Transfer Flow.

I am currently having issues with Transfer Flow.

Transfer Flow

package com.template.flows;
import co.paralleluniverse.fibers.Suspendable;
import com.r3.corda.lib.accounts.contracts.states.AccountInfo;
import com.r3.corda.lib.accounts.workflows.UtilitiesKt;
import com.r3.corda.lib.accounts.workflows.flows.RequestKeyForAccount;
import com.template.accountutilities.NewKeyForAccount;
import com.template.contracts.CarContract;
import com.template.states.CarState;
import net.corda.core.contracts.Command;
import net.corda.core.contracts.StateAndRef;
import net.corda.core.contracts.UniqueIdentifier;
import net.corda.core.flows.*;
import net.corda.core.identity.AbstractParty;
import net.corda.core.identity.AnonymousParty;
import net.corda.core.identity.Party;
import net.corda.core.node.services.Vault;
import net.corda.core.node.services.vault.QueryCriteria;
import net.corda.core.transactions.SignedTransaction;
import net.corda.core.transactions.TransactionBuilder;
import net.corda.core.utilities.ProgressTracker;
import static com.template.contracts.CarContract.CID;
import java.lang.reflect.Array;
import java.security.PublicKey;
import java.util.*;
import java.util.stream.Collectors;


@InitiatingFlow
@StartableByRPC
public class CarTransferFlowInitiator extends FlowLogic<String> {

    private final String carVin;
    private final String oldCarOwner;
    private final String newCarOwner;
    private int input;



    public CarTransferFlowInitiator(String carVin,String oldCarOwner, String newCarOwner){
        this.carVin = carVin;
        this.oldCarOwner = oldCarOwner;
        this.newCarOwner = newCarOwner;

    }

    private final ProgressTracker.Step RETRIEVING_NOTARY = new ProgressTracker.Step("Retrieving Notary");
    private final ProgressTracker.Step CREATE_TRANSACTION_INPUT= new ProgressTracker.Step("Creating Transaction Input");
    private final ProgressTracker.Step CREATE_TRANSACTION_OUTPUT= new ProgressTracker.Step("Creating Transaction Output");
    private final ProgressTracker.Step  CREATE_TRANSACTION_BUILDER= new ProgressTracker.Step("Creating transaction Builder");
    private final ProgressTracker.Step SIGN_TRANSACTION = new ProgressTracker.Step("Signing Transaction");
    private final ProgressTracker.Step INITIATE_SESSION = new ProgressTracker.Step("Initiating session with counterparty");
    private final ProgressTracker.Step FINALIZE_FLOW = new ProgressTracker.Step("Finalizing the flow");


    private final ProgressTracker progressTracker = new ProgressTracker(
            RETRIEVING_NOTARY,
            CREATE_TRANSACTION_OUTPUT,
            CREATE_TRANSACTION_BUILDER,
            SIGN_TRANSACTION,
            INITIATE_SESSION,
            FINALIZE_FLOW
    );


    @Override
    public ProgressTracker getProgressTracker() {
        return progressTracker;
    }


    public StateAndRef<CarState> checkForCarStates(UUID accountId) throws FlowException {

        // This returns the old owners unconsumed state
        QueryCriteria.VaultQueryCriteria criteria = new QueryCriteria.VaultQueryCriteria().withExternalIds(Arrays.asList(accountId)).withStatus(Vault.StateStatus.UNCONSUMED);

        //QueryCriteria generalCriteria = new QueryCriteria.VaultQueryCriteria(Vault.StateStatus.UNCONSUMED);

        //QueryCriteria generalCriteria = new QueryCriteria.VaultQueryCriteria(Vault.StateStatus.UNCONSUMED);

        List<StateAndRef<CarState>> CarStates = getServiceHub().getVaultService().queryBy(CarState.class, criteria).getStates();

        boolean inputFound = false;
        int t = CarStates.size();
        input = 0;
        for (int x = 0; x < t; x++) {
           if (CarStates.get(x).getState().getData().getCarVIN().equals(carVin)) {
           // if (CarStates.get(x).getState().getData().getLinearId().getExternalId().equals(linearId.getExternalId())) {
                input = x;
                inputFound = true;
            }
        }


        if (inputFound) {
            System.out.println("\n Input Found");


        } else {
            System.out.println("\n Input not found");
            throw new FlowException();
        }

        return CarStates.get(input);
    }


    @Suspendable
    public String call() throws FlowException {

        //Retrieve the notary identity from the network map
        progressTracker.setCurrentStep(RETRIEVING_NOTARY);
        Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);


        //<-------------------------------------- NEW CODE ------------------------------>

        // Get the oldOwnerAccount info here
        AccountInfo oldOwnerAccountInfo = UtilitiesKt.getAccountService(this).accountInfo(oldCarOwner).get(0).getState().getData();

        //Get the party here
        AnonymousParty oldOwnerAccount = subFlow(new RequestKeyForAccount(oldOwnerAccountInfo));


        // Get the newOwnerAccount info here
        AccountInfo newOwnerAccountInfo = UtilitiesKt.getAccountService(this).accountInfo(newCarOwner).get(0).getState().getData();

        //Get the party here
        AnonymousParty newOwnerAccount = subFlow(new RequestKeyForAccount(oldOwnerAccountInfo));


        //Create transaction components both input and output for this application
        progressTracker.setCurrentStep(CREATE_TRANSACTION_OUTPUT);
        StateAndRef<CarState> inputState = null;
        inputState = checkForCarStates(oldOwnerAccountInfo.getIdentifier().getId());

        System.out.println(inputState.getState().getData().getCarMake()+" "+inputState.getState().getData().getCarVIN());


        //Issuer is Toyota
        //Owner is  AutoSmart

        //AnonymousParty issuer = inputState.getState().getData().getIssuer();
        PublicKey issuerKey = inputState.getState().getData().getIssuer().getOwningKey();

        PublicKey newOwnerKey = newOwnerAccount.getOwningKey();

        //Create  transaction components both input and output for this application
        progressTracker.setCurrentStep(CREATE_TRANSACTION_OUTPUT);
        //CarState outputState = new CarState(carMake,carModel,carYear,carMileage,carVin,issuer,carOwner);

//        String carMake = inputState.getState().getData().getCarMake();
//        String carModel = inputState.getState().getData().getCarModel();
//        int carYear = inputState.getState().getData().getCarYear();
//        double carMile  = inputState.getState().getData().getCarMileAge();
//        String carVIN = inputState.getState().getData().getCarVIN();
//        AnonymousParty carIssuer = inputState.getState().getData().getIssuer();




        // This creating a new output ?
       // CarState outputState = new CarState(carMake,carModel,carYear,carMile, carVin,carIssuer,newOwnerAccount);


//        System.out.println(outputState.getParticipants());

        // Thi swill collect signature from Issuer, Old owner and New Owner


        // check what to provide here
        //List<PublicKey> requiresSigners = Arrays.asList(getOurIdentity().getOwningKey());

//        List<PublicKey> requiredSigners = inputState.getState().getData().getParticipants()
//                .stream().map(AbstractParty::getOwningKey)
//                .collect(Collectors.toList());
//        requiredSigners.add(newOwnerAccount.getOwningKey());

        List<PublicKey> requiredSigners = Arrays.asList(issuerKey,oldOwnerAccount.getOwningKey(),newOwnerKey);



        System.out.println(issuerKey+","+oldOwnerAccount.getOwningKey()+","+newOwnerKey);

        final Command<CarContract.Transfer> txCommand = new Command<>(
                new CarContract.Transfer(),
                requiredSigners
        );

        final TransactionBuilder txBuilder = new TransactionBuilder(notary)
                .addInputState(inputState)
                .addOutputState(inputState.getState().getData().withNewOwner(newOwnerAccount), CID)
                .addCommand(txCommand);

        // Create the transaction builder here and add compenents to it
        progressTracker.setCurrentStep(CREATE_TRANSACTION_BUILDER);

        // Sign the transaction
        progressTracker.setCurrentStep(SIGN_TRANSACTION);
        txBuilder.verify(getServiceHub());
        final SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder, getOurIdentity().getOwningKey());

        System.out.println("Current nodes identity: " +getOurIdentity().getName().getOrganisation());

        System.out.println("Signed transaction by old owner"+": "+signedTx);

        // <----------------------------------------------------------------------->
        // Code stops here
        List<FlowSession> sessions = new ArrayList<>();
        AccountInfo issuerAccount = UtilitiesKt.getAccountService(this).accountInfo("account1").get(0).getState().getData();
        sessions.add(initiateFlow(issuerAccount.getHost()));
        sessions.add(initiateFlow(newOwnerAccountInfo.getHost()));


        // Create session with counterparty
        progressTracker.setCurrentStep(INITIATE_SESSION);
        System.out.println("Owner Host name is: "+newOwnerAccountInfo.getHost().getName().getOrganisation()+" Account is: "+newOwnerAccountInfo.getName());




        System.out.println("Started session with new owner");


        SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(
                signedTx, sessions, Collections.singleton(getOurIdentity().getOwningKey())));

//
//        final SignedTransaction fullySignedTx = subFlow(
//                new CollectSignaturesFlow(signedTx, sessions, CollectSignaturesFlow.Companion.tracker()));
//
//        SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(
//                signedTx, sessions, CollectSignaturesFlow.tracker()));

//        SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(
//                signedTx, sessions));


        System.out.println("Passed the fullySignedTx section of the code.");

        //Finalizing  the transaction
        progressTracker.setCurrentStep(FINALIZE_FLOW);
        SignedTransaction sTx = subFlow(new FinalityFlow(fullySignedTx,sessions));

        System.out.println("Getting sign from initiator");
        return "Transfer Completed";
    }
}

State

package com.template.states;

import com.template.contracts.CarContract;
import net.corda.core.contracts.BelongsToContract;
import net.corda.core.contracts.ContractState;
import net.corda.core.contracts.LinearState;
import net.corda.core.contracts.UniqueIdentifier;
import net.corda.core.identity.AbstractParty;
import net.corda.core.identity.AnonymousParty;
import net.corda.core.serialization.ConstructorForDeserialization;
import org.jetbrains.annotations.NotNull;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;


@BelongsToContract(CarContract.class)

public class CarState implements ContractState, LinearState {

    private String carMake;
    private String carModel;
    private int carYear;
    private double carMileAge;
    private String carVIN;
    private AnonymousParty issuer;
    private AnonymousParty owner;
    private UniqueIdentifier linearId;
    private List<AbstractParty> participants;


    public CarState(String carMake, String carModel, int carYear, double carMileAge, String carVIN, AnonymousParty issuer, AnonymousParty owner) {
        this.carMake = carMake;
        this.carModel = carModel;
        this.carYear = carYear;
        this.carMileAge = carMileAge;
        this.carVIN = carVIN;
        this.issuer = issuer;
        this.owner = owner;
        this.linearId = new UniqueIdentifier();

    }

    @ConstructorForDeserialization
    public CarState(String carMake, String carModel, int carYear, double carMileAge, String carVIN, AnonymousParty issuer, AnonymousParty owner, UniqueIdentifier linearId) {
        this.carMake = carMake;
        this.carModel = carModel;
        this.carYear = carYear;
        this.carMileAge = carMileAge;
        this.carVIN = carVIN;
        this.issuer = issuer;
        this.owner = owner;
        this.linearId = linearId;

    }


    public String getCarMake() {
        return carMake;
    }

    public String getCarModel() {
        return carModel;
    }

    public int getCarYear() {
        return carYear;
    }

    public double getCarMileAge() {
        return carMileAge;
    }

    public String getCarVIN() {
        return carVIN;
    }

    public AnonymousParty getIssuer() {
        return issuer;
    }

    public AnonymousParty getOwner() {
        return owner;
    }



    @NotNull
    @Override
    public List<AbstractParty> getParticipants() {
        return Arrays.asList(issuer,owner);

    }

    @NotNull
    @Override
    public UniqueIdentifier getLinearId() {
        return linearId;
    }

    public CarState withNewOwner(AnonymousParty newOwner){
        return new CarState(carMake,carModel,carYear,carMileAge, carVIN,issuer,newOwner, linearId);
    }


}

1

1 Answers

0
votes

There are many things in your flow that need to be changed:

  1. Your approach of searching for a car by the owner then looping through the list until you find the state with the VIN that you want is not good. What if that account had 100,000 cars? You're going to loop through 100,000 records until you find the one with the VIN that you want? Instead, create a custom schema for your car state so you can create a query criteria that filters by VIN. See an example of how to create a custom schema here, here, and here.
  2. Your car state should implement LinearState instead of just ContractState. This will allow you to "update" your state. LinearState has a linearId attribute which is unique on the network level. So when you want to update a car, you consume as an input then you create the updated version as an output; the output car must have the exact same linearId, this way you can track the different version of that car as it changes its attributes values (you query the car by that linearId).
  3. Following step #2, your car state should have 2 constructors; one that doesn't take linearId as an input parameter and inside the constructor you assign a random value to it (this constructor is used when you issue a car, the output state is created using that constructor); then you need a second constructor that takes linearId as an input parameter and is marked with the annotation @ConstructorForDeserialization; this constructor is used in transfer when creating the output car (i.e. the car with the new owner), you will pass to it the linearId of the input car; this way the output car is considered as the updated version of the input car (because they have the same linearId).
  4. As for your question about the required signer; it's you who decides who's the required signer for each command inside the contract (which you didn't share the code of in your question). Logically speaking, when you issue a new car, only the issuer should be required; and when you transfer a car, only the current (old) owner is required to sign.
  5. The assumption is that the initiator of the transfer flow is the current owner of the car (you don't want someone transferring your car without your consent). So in your case the initiator of the flow (i.e. the old owner) is the only required signer (if that's what your contract says); so signing the transaction locally is sufficient.
  6. You still need to create a FlowSession with the node of the new owner; so that finality flow sends the finalized transaction/state and the responder flow (i.e. new owner) receives the finalized transaction/state.