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 multi-signature transaction defines a workflow where a transaction would require more than two signatures in order to reach a finalised state.
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:
DAML scenarios also provide a nice way to view the resulting state.
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.
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:
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:
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.
About the Authors:
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
Chief Technology Officer, Block8
Samuel G Brooks is Block8's Chief Technology Officer. He is an expert in developing decentralised software products and has designed numerous solutions for startups, enterprise, government and OpenTech since 2014.
Samuel regularly speaks at technology conferences, meetups and podcasts, and holds several advisory positions on technical industry boards and committees. He is a also a heavy contributor to blockchain and fintech-related public inquiry and writes about the nature and benefits of distributed ledger technology on our blog.
Samuel holds a degree in Electrical Engineering from UNSW, and has stayed close to both the code and the latest research ever since encountering Bitcoin in 2011.
Get the latest news on our products or learn what's happening in our guilds.