Rao Zoraiz Mahmood & Samuel G Brooks
Rao Zoraiz Mahmood & Samuel G Brooks

10.11.2020. in Technology, Solutions

Block8 Rates: R3’s Corda vs Digital Asset’s DAML

Part Four: Code Compare - A Multisignature Transaction

Distributed Ledger Technology (DLT) in the enterprise space has seen a number of new players in recent years, providing greater variety for businesses to select an appropriate technology for their needs. Often, these choices are not made on the basis of the merits of the technology alone, but rather on existing business relationships and, sometimes, hype.

The most well-known DLT technologies available for enterprise solutions today include Hyperledger Besu (an Ethereum client, previously Pantheon), R3’s Corda, Digital Asset’s DAML and Hyperledger Fabric. In each part of the Block8 Rates series, we will attempt to provide a direct comparison between two players in the scene against several key metrics.

In particular, we’ll be taking a look at writing smart contracts in R3’s Corda compared to Digital Asset’s DAML.

Other parts in this series:

Part One - Overview of State and Transactions

Part Two - DevEx - Learnability and Documentation

Part Three - Functionality - What Can It Do?

Although we have already modelled a similar workflow example in Corda and DAML with the IOU example, we shall also model some common patterns in distributed ledger systems.

A Multisignature Transaction

A multi-signature transaction defines a workflow where a transaction would require more than two signatures in order to reach a finalised state.

DOWNLOAD THE FULL SERIES HERE

DAML

In this DAML example, we specify a list of users who must be signatories in order to reach a final state SignatureSet. Every user must create a Signature object, with the appropriate parameters. The hoster can then execute the GetSigSet method, resulting in the creation of SignatureSet. If not all of the members required have created the Signature object, then the SignatureSet fails to be created. There are no race conditions and it is completely async, meaning anyone can sign in any order.

– Code by Digital Asset's Bernhard Elsner


daml 1.2
 
module Multisig where
 
import DA.Next.Set as S
import DA.Action
 
template Signature
 with
   sig : Party
   obs : Set Party
   hoster : Party
 where
   signatory sig
   observer obs
   key this : Signature
   maintainer key.sig
 
   nonconsuming choice TransferSignature
     : ContractId SignatureSet
     with
       p : Party
       sscid : ContractId SignatureSet
     controller p
       do
         exercise sscid Sign with
           newSig = sig
  
   controller hoster can
     nonconsuming GetSigSet
       : ContractId SignatureSet
       do
         acc <- create SignatureSet with
           sigs = S.singleton sig
           toSign = obs
         foldlA
           (\sscid p -> exerciseByKey @Signature (this with sig = p)
             TransferSignature with p = sig, ..)
           acc
           (S.toList obs)
           
template SignatureSet
 with
   sigs : Set Party
   toSign : Set Party
 where
   signatory sigs
 
   choice Sign
     : ContractId SignatureSet
     with
       newSig : Party
     controller newSig
     do
       assert (member newSig toSign)
       create this with sigs = S.insert newSig sigs
 
 
test = scenario do
   alice <- getParty "Alice"
   bob <- getParty "Bob"
   charlie <- getParty "Charlie"
   -- This line converts a list to a set
   let ps = S.fromList[alice, bob, charlie]
   -- Charlie Creates an instance of Signature, with himself as the hoster
   sCharlie <- submit charlie $ create $ Signature charlie ps charlie
   -- Alice Creates an instance of Signature, with charlie as the hoster
   sAlice <- submit alice $ create $ Signature alice ps charlie
   -- Bob Creates an instance of Signature, with charlie as the hoster
   sBob <- submit bob $ create $ Signature bob ps charlie
   -- All parties have signed, now Charlie and execute `GetSigSet`.
   -- This creates an instance of GetSigSet for which Charlie, Alice and Bob are all signatories
   -- If any party has not signed, as specified by the contract, the `GetSigSet` fails
   submit charlie $ exercise sCharlie GetSigSet
 

 

To get a full understanding of the above code, check out the official documentation. We also see at the bottom of the code Scenario results. Here we are actually running a sample workflow unit test to ensure that the code is working as intended. Looking at the test, there are a few things we do in the sample workflow:

  1. Create parties for Alice, Bob and Charlie (this generates fake parties for testing purposes).
  2. Create a set containing Alice, Bob, Charlie.
  3. Create a Signature object as Charlie, with parameters sig = Charlie; obs = Set(Charlie, Bob, Alice); hoster = Charlie.
  4. Create a Signature object as Alice, with parameters sig = Alice; obs = Set(Charlie, Bob, Alice); hoster = Charlie.
  5. Create a Signature object as Bob, with parameters sig = Bob; obs = Set(Charlie, Bob, Alice); hoster = Charlie.
  6. Execute the GetSigSet method as Charlie, which creates the SignatureSet successfully.

DAML scenarios also provide a nice way to view the resulting state.

enter image description here

 

We can see that there is one instance of SignatureSet and 3 instances of Signature, as expected.

Now let’s try running a scenario which we expect to fail


test = scenario do
   alice <- getParty "Alice"
   bob <- getParty "Bob"
   charlie <- getParty "Charlie"
   -- This line converts a list to a set
   let ps = S.fromList[alice, bob, charlie]
   -- Charlie Creates an instance of Signature, with himself as the hoster
   sCharlie <- submit charlie $ create $ Signature charlie ps charlie
   -- Alice Creates an instance of Signature, with charlie as the hoster
   -- sAlice <- submit alice $ create $ Signature alice ps charlie
   -- Bob Creates an instance of Signature, with charlie as the hoster
   sBob <- submit bob $ create $ Signature bob ps charlie
   -- All parties have signed, now Charlie and execute `GetSigSet`.
   -- This creates an instance of GetSigSet for which Charlie, Alice and Bob are all signatories
   -- If any party has not signed, as specified by the contract, the `GetSigSet` fails
   submit charlie $ exercise sCharlie GetSigSet
   
 

We can already see the red underline for test. This means there was an error that occurred in the scenario. That is expected as we actually commented out Alice’s signature and Charlie attempted to GetSigSet.

Checking the scenario results, we see this:


Scenario execution failed on commit at Multisig:74:5:
CRASH: Key GlobalKey(Identifier(.....) not found

Ledger time: 1970-01-01T00:00:00Z

Partial transaction:
 Failed exercise (Multisig:16:9):
   exercise GetSigSet on #0:0 (Multisig:Signature)
   with
 Sub-transactions:
    #1
    └─> create Multisig:SignatureSet
        with
          sigs =
            (DA.Next.Set:Set@cb59f...4751c14fdba6b0347d492cea with
               textMap = Map[Charlie->{}]);
          toSign =
            (DA.Next.Set:Set@cb59fd7...b4751c14fdba6b0347d492cea with
               textMap = Map[Alice->{}, Bob->{}, Charlie->{}])

 

Here it says Failed exercise (Multisig:17:9): exercise GetSigSet …

What this is saying is that Charlie’s attempt at exercising the GetSigSet method failed, because Alice hasn’t signed yet.

Let’s take a look at a multisig workflow in Corda.

Corda

In Corda, we shall attempt to create a final state known as FullySignedState. This state object takes a list of users as an argument and requires that all the users specified must sign in order for the state to be created. Recall that every State must have an associated Contract. We shall call this FullySignedContract - this is where we specify the rules that all the parties must follow when using the state. Let’s take a look at our State class.


@BelongsToContract(FullySignedContract.class)
@CordaSerializable
public class FullySignedState implements ContractState {
   private AbstractParty hoster;
   private List signers;
 
   @ConstructorForDeserialization
   public FullySignedState(AbstractParty hoster, List signers) {
       this.signers = signers;
       this.hoster = hoster;
   }
 
   public AbstractParty getHoster() {
       return hoster;
   }
 
   public List getSigners() {
       return signers;
   }
 
   @Override
   public List getParticipants() {
       List participants = new ArrayList<>();
       participants.add(hoster);
       for (AbstractParty apart : signers){
           participants.add(apart);
       }
       return participants;
   }
}

 

Our FullySignedState specifies a hoster who initiates the workflow, as well as a list of the required signers. Let’s take a look at the Contract that governs this State.

public class FullySignedContract implements Contract {
   public static final String ID = "com.template.multisigapp.FullySignedContract";
 
   public static class Commands implements CommandData {
       public static class Create extends Commands{}
   }
 
   @Override
   public void verify(LedgerTransaction tx) {
       final CommandWithParties command = requireSingleCommand(tx.getCommands(), Commands.class);
       if (command.getValue() instanceof FullSignedContract.Commands.Create) {
           // Constraints on the shape of the transaction.
           if (!tx.getInputs().isEmpty())
               throw new IllegalArgumentException("No inputs should be consumed when creating");
           if (!(tx.getOutputs().size() == 1))
               throw new IllegalArgumentException("There should be one output state");
 
           // FullySignedState-specific constraints.
           final FullySignedState output = tx.outputsOfType(FullySignedState.class).get(0);
           if (output == null) throw new IllegalArgumentException("Output is not of type FullySignedState");
 
           // Signatory-specific constraints.
           final List signersProvided = command.getSigners();
           final List expectedSigners = new ArrayList<>();
           expectedSigners.add(output.getHoster().getOwningKey());
           List outputSigners = output.getSigners();
           for (AbstractParty currSigner : outputSigners){
               expectedSigners.add(currSigner.getOwningKey());
           }
           if (!(signersProvided.containsAll(expectedSigners)))
               throw new IllegalArgumentException("The Required Signers are not Present!");
 
       } else {
           throw new IllegalArgumentException("Command is not of type Create!");
       }
   }
}

The contract just checks that a transaction which uses the FullySignedState state conforms to certain rules. In this case, the contract checks that if a transaction is of type Create, we make sure the transaction has 0 input states and 1 output state. The 1 output state must be of type FullySignedState. Everyone specified in the FullySignedState's signers parameter must also have signed the transaction, otherwise it throws an Exception with the error message “The Required Signatories are not present!”.

If you read through the Context section, you would recall that transactions are executed through Flows. Flows can be written by any entity in the network however they wish. That’s why it’s important to have strong validation rules in the Contract as malicious users may try to write bad flows which would result in states benefiting them, but if the Contract governing a state is strong enough, this is not possible. Let’s take a look at a possible Multisignature Flow:

@InitiatingFlow
@StartableByRPC
public class FullySignedInitiator extends FlowLogic {
   private AbstractParty hoster;
   private ArrayList signers;
   /**
    * The progress tracker provides checkpoints indicating the progress of the flow to observers.
    */
   private final ProgressTracker progressTracker = new ProgressTracker();
 
   public FullySignedInitiator(AbstractParty hoster, ArrayList signers) {
       this.hoster = hoster;
       this.signers = signers;
   }
 
   @Override
   public ProgressTracker getProgressTracker() {
       return progressTracker;
   }
 
   /**
    * The flow logic is encapsulated within the call() method.
    */
   @Suspendable
   @Override
   public SignedTransaction call() throws FlowException {
       // We retrieve the notary identity from the network map.
       Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
 
       if (!(getOurIdentity().getOwningKey().equals(hoster.getOwningKey())))
           throw new IllegalArgumentException("Hoster must be the initiator!");
 
       // We create the proposed output state
       FullySignedState outputState = new FullySignedState(hoster, signers);
       //We add the signatories required to `requiredSigners`
       List requiredSigners = new ArrayList<>();
       requiredSigners.add(hoster.getOwningKey());
       for (AbstractParty signer : signers){
           requiredSigners.add(signer.getOwningKey());
       }
       /*
         We specify the command we want (in this case Create). Commands
         just give us more flexibility with the rules we specify in the
         associated contract class
       */
       Command command = new Command<>(new FullySignedContract.Commands.Create(), requiredSigners);
 
       // We create a transaction builder and add the components.
       TransactionBuilder txBuilder = new TransactionBuilder(notary)
               .addOutputState(outputState, FullySignedContract.ID)
               .addCommand(command);
 
       /*
       Verifying the transaction is correct according to the contract rules.
       This is done automatically upon signing anyway
       */
       txBuilder.verify(getServiceHub());
 
       //Create flow sessions with all the people we need to sign and exchange identities
       ArrayList openSessions = new ArrayList<>();
       for (AbstractParty currSigner : signers){
           FlowSession currSession = initiateFlow((Party) currSigner);
           openSessions.add(currSession);
       }
       //Sign the initial transaction
       SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder);
 
       //Obtain Signatures from all the required parties
       SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(
           signedTx, openSessions, CollectSignaturesFlow.tracker()));
 
       // Finalising the transaction.
       return subFlow(new FinalityFlow(fullySignedTx, openSession));
   }
}

 

The comments explain the processes occurring. When the hoster wishes to interact with other nodes, in this case the required signatories, he must create FlowSession's with them. This creates a bilateral communication channel. The hoster can then execute the CollectSignaturesFlow (which is essentially a subroutine that executes predefined communication exchange between nodes). Now the responding node must also have a flow set up to respond to the initiator, who in this case is the hoster.

@InitiatedBy(FullySignedInitiator.class)
public class FullySignedResponder extends FlowLogic {
   private final FlowSession otherPartySession;
 
   public FullySignedResponder(FlowSession otherPartySession) {
       this.otherPartySession = otherPartySession;
   }
 
   @Suspendable
   @Override
   public Void call() throws FlowException {
       class SignTxFlow extends SignTransactionFlow {
           private SignTxFlow(FlowSession otherPartySession) {
               super(otherPartySession);
           }
 
           @Override
           protected void checkTransaction(SignedTransaction stx) {
               requireThat(require -> {
                   ContractState output = stx.getTx().getOutputs().get(0).getData();
                   // require.using("I won't accept anything", false);
                   return null;
               });
           }
       }
      
       SecureHash expectedTxId = subFlow(new SignTxFlow(otherPartySession)).getId();
 
       subFlow(new ReceiveFinalityFlow(otherPartySession, expectedTxId));
 
       return null;
   }
}
 

We see that the person responding to the hoster must have a flow which directly responds to the hoster. The first thing that is done is:

subFlow(new SignTxFlow(otherPartysession)).getId() 

When the hoster calls the CollectSignaturesFlow, this SignTxFlow responds by executing the checkTransaction method. This is where the responder would place any sort of validation which they personally want to implement. For example, if they don’t want to be a signatory for any transaction of this state, they can just return false in the checkTransaction method.

Running tests can be as simple as:

 @Test
public void fullySignedTxFlow() throws Exception{
      fullySignedFlow(a, Arrays.asList(b, c, d));
        network.waitQuiescent();
}

This is part of a larger code segment, but would run the FullySignedInitiatior as a (the initiator) and host the FullySignedResponder as b, c, d (the required signatories).

Running the test would prove successful:

enter image description here

Let’s say we had a malicious hoster, who wanted to have multiple users involved in the FullySignedState but didn’t want to collect their signatures because he knows they may decline. He would implement a flow which does not collect all of their signatures. However, the FullySignedContract would catch this. Running the same test results in Exception error:

enter image description here

Conclusion

A direct parallel of a multiparty workflow accentuates the succinct nature of DAML. The automation provided by Corda also brings greater coding requirements. This tradeoff must be made on a case by case basis which our team at Block8 are experts in.

New call-to-action

About the Authors:

Rao-Zoraiz-Mahmood-Software-Engineer-Block8

Rao Zoraiz Mahmood
Software Engineer, Block8

Zoraiz Mahmood is a Software Engineer at Block8, working on emerging technology.
His current research and development interest focuses on protocol development for distributed systems (blockchain), and is currently writing a paper on the Performance Comparison of Blockchain Data Storage Models.

Zoraiz regularly creates blockchain technical and related content, having written multiple technical articles, hosted podcasts and produced YouTube videos.

When not generating value, Zoraiz can be found out and about exploring the Australian landscape.

 

Samuel_Brooks_Block8_CTOSamuel Brooks
Chief Technology Officer, Block8

Samuel Brooks is an expert in connecting next-gen technology with current-gen business problems, with particular specialisation in the development of distributed ledger systems, having designed many DLT-based solutions and authored and contributed to multiple public submissions from both industry and government.

He is also an active member of the Australian blockchain community, including regularly speaking at technology conferences, meetups and podcasts, and contributing to industry and International Standards committees.
Samuel holds a degree in Electrical Engineering from UNSW and has been working hands-on with blockchains since 2014.

Subscribe to our Newsletter

Get the latest news on our products or learn what's happening in our guilds.