블로그

Block8 심층 분석 시리즈 : R3’s Corda vs Digital Asset’s DAML

socl 2020. 9. 21. 15:03

1 부 : 상태 및 트랜잭션 개요

기업 환경의 DLT(Distributed Ledger Technology)는 최근 몇 년 동안 많은 새로운 플레이어가 등장하여 기업이 필요에 따라 적절한 기술을 선택할 수 있도록 다양한 기능을 제공합니다. 종종 이러한 선택은 기술의 장점만을 기반으로 하는 것이 아니라 기존의 비즈니스 관계와 때로는 과대광고를 기반으로 합니다.

오늘날 엔터프라이즈 솔루션에 사용할 수 있는 가장 잘 알려진 DLT 기술에는 Hyperledger Besu (이전 Pantheon의 Ethereum 클라이언트), R3의 Corda, Digital Asset의 DAML 및 Hyperledger Fabric 등이 있습니다. Block 8 Rates 시리즈의 각 부분에서 우리는 몇 가지 주요 지표에 대해 현장의 두 플레이어를 직접 비교하려고 노력할 것입니다.

특히 Digital Asset의 DAML과 비교하여 R3의 Corda에서 스마트 계약을 작성하는 방법을 살펴보겠습니다. 이는 사용된 기본 원장이 이 블로그의 주요 초점이 아님을 의미합니다.

**이 시리즈의 다른 부분 : **

2부-DevEx : 문서화, 개발 용이성 및 테스트 가능성

3부-기능 : 무엇을 할 수 있습니까?

4부-코드 비교 : 다중 서명 트랜잭션

이러한 분산 원장 기술의 뉘앙스를 이해하기 위해 상태 및 상태 변이(트랜잭션)를 관리하는 방법을 살펴봄으로써 이러한 기술을 사용할 수 있는 방법을 간략하게 설명하는 것으로 시작합니다. 전체 구현 세부 사항은 다루지 않겠지만, 기술에 익숙하지 않은 사용자가 더 잘 이해할 수 있도록 코드 조각이 표시됩니다. 두 기술의 구현에 익숙하다면 2부로 건너뛸 수 있습니다.

상태(State)

Corda와 DAML에서 동일한 상태를 모델링합니다. 우리가 대표할 상태를 IOU라고 합니다. IOU에는 발급자, 소유자 및 값의 세 가지 매우 간단한 속성이 있습니다. 기본적인 유사점은 준비은행이 돈을 만들어(발행자) ComBankA(소유자)에게 제공하는 것입니다. 그런 다음 ComBankA는 IOU의 소유권을 클라이언트 중 하나로 이전할 수 있습니다. 이 경우 소유자는 변경되지만 발행자와 가치는 동일하게 유지됩니다. 아래 코드 조각은 몇 가지 기능이 누락되었으며 복잡성을 숨기고 이해를 돕기 위한 더 큰 코드의 일부입니다. 또한 Corda는 문서에 반영된 Kotlin과 Java에서 코드를 작성할 수 있는 옵션을 제공합니다. 데모에서는 주류 언어인 Kotlin보다 Java를 사용할 것입니다.

Corda
Corda의 상태는 단순히 Java 클래스로 모델링됩니다. 클래스의 인스턴스 (및 몇 가지 추가 단계)를 만드는 것은 상태를 원장에 넣는 프로세스입니다.

public class IOUState implements ContractState {
    private int value;  
    private AbstractParty issuer;
    private AbstractParty owner;

    public IOUState(int value, AbstractParty issuer, AbstractParty owner) {  
          this.value = value;  
          this.issuer = issuer;  
          this.owner = owner;  
    }  

      public int getValue() {  
          return value;  
    }  

      public AbstractParty getIssuer() {  
          return issuer;  
    }  

      public AbstractParty getOwner() {  
          return owner;  
    } 
      @Override
      public  List getParticipants() {
          return  Arrays.asList(issuer, owner);
      } 
  }

보시다시피 코드는 비교적 간단합니다. ContractState 인터페이스를 구현하는 IOUState 클래스가 있습니다. 이것은 Corda에서 모델링된 모든 상태에 필요합니다. 인터페이스는 상태를 볼 수 있는 사람을 정의하는 getParticipants() 메서드를 구현해야 합니다. 이 경우 IOUState의 참가자를 해당 IOUState의 발급자 및 소유자로 설정합니다. IOUState에는 값, 발급자 및 소유자를 취하는 생성자가 있습니다. 발급자와 소유자는 Corda에서 당사자를 나타내는 방법 인 AbstractParty로 저장됩니다. 또한 Kotlin 변형은 아래에 표시된 것처럼 모든 매개 변수에 대해 명시적인 게터(getter)가 필요하지 않고 더 간결하다는 점에 유의합니다.

class IOUState(val value: Int,
              val issuer: Party,
              val owner: Party) : ContractState {
   override val participants get() = listOf(issuer, owner)
}

DAML
DAML은 Haskell에서 영감을 받아 DLT 플랫폼을 위해 특별히 제작된 프로그래밍 언어입니다. Corda가 클래스를 사용하고, DAML은 템플릿을 사용합니다. 템플릿은 인터페이스, 추상 클래스 및 상속과 같은 OO(Object Oriented) 구조가 없는 클래스로 생각할 수 있는 기본 제공 DAML 유형입니다. 이로 인해 Corda가 Java의 OO의 힘으로 인해 모델링에서 우수하다고 생각할 수 있지만, Corda는 현재 생성자를 사용하여 객체를 생성하는 사용자 정의 직렬 변환기(serializer)를 사용하여 OO 관행을 최대한 활용할 수 있는 기능을 제한합니다. 클래스와 템플릿은 모두 생성자에서 개체의 인스턴스를 만들 수 있는 기본 구조를 정의합니다.

template Iou
    with
        issuer :  Party
        owner :  Party
        value :  Decimal
    where
          signatory issuer, owner

Iou의 인스턴스는 Java 생성자와 유사하게 인스턴스화 되지만 DAML은 매개 변수가 생성자를 정의한다고 가정하므로 실제로 생성자를 정의할 필요는 없습니다. 보시다시피 동일한 매개 변수 발급자, 소유자 및 값을 저장합니다. Corda 상태는 상태를 볼 수 있는 사람을 결정하는 방법 인 getParticipants() 메서드를 구현해야 합니다. DAML에서 이것은 필수 서명 필드(signatory)를 통해 수행됩니다. Corda 예제와 마찬가지로 이를 발행자 및 소유자로 설정합니다.

거래(Transactions)

구현과 관련하여 트랜잭션은 Corda와 DAML 간의 차이를 실제로 확인하기 시작하는 곳입니다. Corda와 DAML은 트랜잭션이 일부 이전 상태를 사용하여 미래 상태를 생성할 수 있도록 설계되었습니다. 이전 상태를 사용하지 않는 트랜잭션도 있을 수 있습니다.

Corda
Corda에는 상태, 흐름 및 계약의 세 가지 유형의 파일이 있습니다. IOUState 예제에서 이미 상태 파일을 살펴보았습니다. 다음 유형은 Flow 파일입니다.

Flows
흐름은 공유 상태를 만들고 변경하는 방법입니다. 원장에 상태가 존재하려면 흐름을 거쳐야 합니다. 예를 들어, 우리가 준비은행(발행자)이고 IOU를 생성하여 50,000 가치의 CommercialBankA(소유자)에 제공하려고 합니다. 이것은 Corda 노드에 대한 API 호출을 작성하여 시작할 수 있습니다. Java의 코드를 살펴보겠습니다.

@InitiatingFlow
@StartableByRPC
public class IOUFlow extends FlowLogic {
    private final Integer iouValue;
    private final Party owner;
    private final Party issuer;

    public IOUFlow(Party issuer, Integer iouValue, Party owner) {
        this.iouValue = iouValue;
        this.owner = owner;
        this.issuer = issuer;
    }

    @Suspendable
    @Override
    public SignedTransaction call() throws FlowException {
        // 네트워크 맵에서 공증인 ID를 검색합니다.
        Party notary = getServiceHub().getNetworkMapCache().getNotaryIdentities().get(0);
        // 트랜잭션 컴포넌트를 만듭니다.
        IOUState outputState = new IOUState(iouValue, issuer, owner);
        List requiredSigners = Arrays.asList(issuer.getOwningKey(), owner.getOwningKey());
        Command command = new Command<>(new IOUContract.Commands.Create(), requiredSigners);

        // 트랜잭션 빌더를 만들고 컴포넌트를 추가합니다.
        TransactionBuilder txBuilder = new TransactionBuilder(notary)
                .addOutputState(outputState, IOUContract.ID)
                .addCommand(command);

        // 거래 서명.
        SignedTransaction signedTx = getServiceHub().signInitialTransaction(txBuilder);

        // 상대방과의 세션 생성.
        FlowSession otherPartySession = initiateFlow(owner);

        // 상대방의 서명을 얻습니다.
        SignedTransaction fullySignedTx = subFlow(new CollectSignaturesFlow(
                signedTx, Arrays.asList(otherPartySession)));

        // 거래 완료.
        return subFlow(new FinalityFlow(fullySignedTx, otherPartySession));
    }
}

이 코드 조각에 대한 완전한 설명은 다루지 않겠지만 이 코드 세그먼트에서 이해해야 하는 몇 가지 중요한 개념이 있습니다. 먼저 생성자가 있음을 알 수 있습니다.

public  IOUFlow(Party issuer, Integer iouValue, Party owner)

이 생성자를 호출하는 것은 IOUFlow를 시작하는데 실제로 필요한 전부이며 RPC로 전체 구현이 완료되면 API 호출을 통해 수행할 수 있습니다. 생성자가 사용되면 시작됩니다.

public SignedTransaction call()

이 함수에서 우리는 원하는 IOUState를 생성하여 트랜잭션을 구축합니다.

IOUState outputState =  new  IOUState(iouValue, issuer, owner);

그런 다음 여러 매개 변수와 함께 outputStateTransactionBuilder 개체에 추가합니다. TransactionBuilder에는 일반적으로 notary, command및 입력 또는 출력 상태가 필요합니다 (command은 이후 섹션에서 다룹니다).

TransactionBuilder txBuilder  =  new TransactionBuilder(notary)
    .addOutputState(outputState, IOUContract.ID)
    .addCommand(command) ;

TransactionBuilder는 입력 상태, 출력 상태, 서명자에 대한 정보 등을 전달하는 객체로 간주될 수 있습니다. 먼저 서명하면 TransactionBuilder 개체가 변경되지 않습니다. 그 후 서명을 위해 소유자에게 보냅니다. 이 경우 서명은 당사자가 제안된 거래에 동의 함을 의미합니다. 이 경우 발행자(Reserve Bank)와 소유자(ComBankA)가 서명해야 합니다. 소유자는 트랜잭션에 대해 자신의 유효성 검사 로직을 구현할 수 있으므로 트랜잭션에 서명하거나 서명하지 않습니다. 소유자는 자신이 호스팅 하는 다른 클래스에서 유효성 검사 로직을 코드 화합니다. 여기서 클래스는 구성 파일을 통해 적절한 CordApp과 연결됩니다. 유효성 검사 로직은 소유자, 가치 등을 확인할 수 있지만 아직 자세히 살펴보지는 않을 것입니다.

노드 간의 통신은 이러한 흐름의 콘텍스트 내에서만 발생하며, 이것이 Corda가 지점 간 개인 정보 보호를 보장하는 방법입니다. 일반적인 작업을 자동화하는 데 사용되는 기본 제공 흐름도 있습니다. 흐름을 통해 사용자는 서명이 필요한 모든 상태 업데이트에 대해 자신만의 개인화된 로직을 작성할 수 있습니다. 이 코드화 된 승인 기준은 Corda에서 제공하는 자동화를 가능하게 합니다.

누구나 어떤 상태로든 자신의 흐름을 만들 수 있다는 점에 유의해야 합니다. 이것은 우려를 불러 일을 킵니다. 누군가가 영향 없이 $1million의 IOU 값을 가진 IOU를 발행하면 어떨까요?
이제 Contract 클래스를 살펴보도록 하겠습니다.

계약(Contracts)
Corda에서 우리가 생성하는 모든 상태 클래스에는 연관된 Contract 클래스가 있어야 합니다.

IOUState 클래스의 경우 IOUContract라고 부를 수 있습니다. 이것은 Corda의 계약에 대한 일반적인 명명 규칙이지만 필수는 아닙니다. Contract 클래스는 생성후에 변경할 수 없으며, 유효한 상태 변형이 되기 위해 State가 실행되야(State must play) 하는 규칙을 정의합니다. 트랜잭션이 실행되는 곳이 아닙니다. 트랜잭션은 Flow 클래스에서 실행됩니다. 누군가 flow에서 트랜잭션에 서명하면 관련된 상태에 대한 계약 클래스가 자동으로 확인됩니다.

public class IOUContract implements Contract {

    public static class Commands implements CommandData {
        public static class Create extends Commands{}

        public static class Transfer extends Commands{}
    }

    @Override
    public void verify(LedgerTransaction tx) {
        final CommandWithParties command = requireSingleCommand(tx.getCommands(), Commands.class);
        if (command.getValue() instanceof IOUContract.Commands.Create) {

            // 거래 형태에 대한 제약.
            if (!tx.getInputs().isEmpty())
                throw new IllegalArgumentException("No inputs should be consumed when issuing an IOU(IOU를 발행 할 때 입력을 사용해서는 안됩니다.)");
            if (!(tx.getOutputs().size() == 1))
                throw new IllegalArgumentException("There should be one output state of type IOUState(IOUState 유형의 출력 상태가 하나 있어야합니다.)");

            // IOU 스펙 제약.
            final IOUState output = tx.outputsOfType(IOUState.class).get(0);
            final Party lender = (Party) output.getIssuer();
            final Party borrower = (Party) output.getOwner();
            if (output.getValue() <= 0)
                throw new IllegalArgumentException("The IOU's value must be non-negative(IOU의 값은 음수가 아니어야합니다.)");
            //서명자 스펙 제약
            //단순성을 위해 제거됨
        }
        else if (command.getValue() instanceof IOUContract.Commands.Transfer){
            System.out.println("Checking if IOUState follows rules for Transfer Command(IOUState가 Transfer Command 규칙을 따르는 지 확인)");
            //TODO
        }else{
            throw new IllegalArgumentException("Command is not of type Transfer or Create!(명령이 전송 또는 작성 유형이 아닙니다!)");
        }
    }
}

모든 계약은 다음 method를 구현해야합니다.

public  void  verify(LedgerTransaction tx)

이 method 실행 된 트랜잭션이 "Create"명령인지 "Transfer"명령인지 확인합니다.

if (command.getValue() instanceof IOUContract.Commands.Create){
    ......
 else if (command.getValue() instanceof IOUContract.Commands.Transfer){  
    ......
}else{  
    throw new IllegalArgumentException("Command is not of type Transfer or Create!(Command가 Transfer 또는 Create 유형이 아닙니다!)");  
}

Command는 서로 다른 상태 변이를 단일 상태 유형과 연관시키는 방법입니다. 계약에 원하는 만큼 명령을 추가할 수 있습니다. 현재 "Create"및 "Transfer"이 있지만 "Destroy"와 같은 명령을 더 추가할 수도 있습니다. 실제로 트랜잭션을 수행할 때 명령 정보를 전달합니다.

명령이 "Create"이면 트랜잭션에 0 개의 입력과 1 개의 출력이 있는지 확인합니다.

if (!tx.getInputs().isEmpty())  
    throw new IllegalArgumentException("No inputs should be consumed when issuing an IOU.");  
if (!(tx.getOutputs().size() == 1))  
    throw new IllegalArgumentException("There should be one output state of type IOUState.");

또한 IOUState의 값이 0보다 큰지 확인합니다.

if (output.getValue() <= 0)  
    throw new IllegalArgumentException("The IOU's value must be non-negative.");

마지막으로 예상 서명자가 서명했는지 확인합니다.

if (requiredSigners.size() != 2)  
    throw new IllegalArgumentException("There must be two signers(두 명의 서명자가 있어야합니다.)");  
if (!(requiredSigners.containsAll(expectedSigners)))  
    throw new IllegalArgumentException("The borrower and lender must be signers.");

Command가 "Transfer"이면 1 개의 입력 상태와 1 개의 출력 상태를 예상할 수 있으며 이 로직을 계약에 추가합니다. "Destroy"명령의 경우 1 개의 입력 상태와 0 개의 출력 상태를 예상할 수 있습니다. 검증 로직의 변동에 따라 명령어를 얼마든지 추가할 수 있습니다. 확인 기능을 비워두고 거래에 대한 유효성 검사를 수행하지 않을 수 있지만, 이렇게 하면 안전하지 않은 상태를 승인할 수 있다는 것을 알 필요가 있습니다.

DAML
Corda에서 상태 변이는 Flows를 실행하여 발생하며, 여기서 흐름의 상태 변이는 계약에 의해 검증됩니다. DAML에는 상태 변이가 발생하는 두 가지 방법이 있습니다. 첫 번째는 템플릿에서 객체(인스턴스)를 생성하는 것으로, 이전 상태가 필요하지 않으며 두 번째는 이러한 객체에 대해 메소드를 실행하는 것입니다. DAML에서는 "선택 실행(exercising a choice)"이라고합니다.

Flows and Contracts가 필요하지 않은 이유는 DAML의 메서드가 Corda의 Flows 및 Contracts 기능을 캡슐화하기 때문입니다. DAML의 모든 메서드는 변경할 수 없으며 템플릿이 작성 될 때 정의됩니다. Corda에서는 계약만 변경할 수 없으며 모든 사용자는 자신의 흐름을 만들 수 있습니다.

앞서 보여 드린 Iou 템플릿을 생각해보십시오. 다음은 더 큰 코드입니다.

template Iou
  with
    issuer : Party
    owner : Party
    value : Decimal
  where

-- C언어의 어설션처럼 Iou 생성에 필요한 사후 조건
    ensure value > 0.0

-- 서명인은 Iou가 발급자와 소유자 모두가 승인 한 경우에만 생성 될 수 있다고 말합니다.
-- 계약을 생성하거나 다시 보관(archive)하는 데 권한이 필요한 당사자입니다.
    signatory issuer, owner  

--소유자가 할 수있는 권한을 정의합니다.
    controller owner can

      Iou_Transfer : ContractId IouTransfer
        with
          newOwner : Party
        do create IouTransfer with 
            iou = this
            newOwner = newOwner

주목해야 할 몇 가지 새로운 사항이 있습니다. 첫 번째는 다음과 같습니다.

ensure value > 0.0

이것은 Corda에서 보여준 Contract 클래스와 비슷한 목적을 재공합니다. 우리는 Iou 객체의 값에 대한 사후 조건을 확실히 설정할 수 있습니다.

또한 거래의 유효성을 위해 서명해야 하는 사람도 명시합니다.

signatory  issuer,  owner

Corda에서 필수 서명자는 Contract 클래스에 지정됩니다.

다음으로 볼 것은 :

   controller owner can

      Iou_Transfer : ContractId IouTransfer
        with
          newOwner : Party
        do create IouTransfer with 
            iou = this
            newOwner = newOwner

소유자만 Iou_Transfer 메서드를 실행할 수 있음을 나타내는 권한을 지정합니다. 이 메서드는 Party 유형의 newOwner를 인수값으로 취하고 결과적으로 IouTransfer가 작성됩니다. 당사자는 사용자의 표현 일뿐입니다. IouTransfer 템플릿 살펴보기 :

template IouTransfer
  with
    iou : Iou
    newOwner : Party
  where
    signatory iou.issuer, iou.owner

    controller iou.owner can
      IouTransfer_Cancel : IouCid
        do create iou

    controller newOwner can
      IouTransfer_Reject : IouCid
        do create iou

      IouTransfer_Accept : IouCid
        do
          create iou with
            owner = newOwner
            observers = []

코드를 살펴보기 전에 무슨 일이 일어나고 있는지 잠시 생각해 보겠습니다. 우리가 가진 첫 번째 상태는 Iou 였고 우리는 새로운 상태 인 IouTransfer를 만드는 선택(choice)(즉, 메서드)을 실행했습니다. 기본 구현은 선택(choice)이 실행될 때 기본 상태가 사용되는 것이므로 이전 Iou 상태가 사용되었습니다. 이제 IouTransfer 상태가 있습니다. 이 상태는 IouTransfer_Accept 메서드를 실행하는 새 소유자에 따라 새 소유자에게 이전되기를 기다리는 전환 상태를 나타냅니다. 다른 작업은 이전 Iou를 대체하는 동일한 매개 변수를 사용하여 새 Iou에 의해 전환 상태를 원래 Iou 상태로 효과적으로 되돌리는 결과를 가져옵니다.

간단한 사례와 비교해 보겠습니다. ReserveBank가 CommericalBankA에 1,000가치의 Iou를 발행하려고 한다고 가정해 보겠습니다. ReserveBank가 할 수 있는 첫번째 일은
issuer = ReserveBank, owner = ReserveBank, value = 1000
으로 Iou 상태를 만드는 것입니다. 왜 우리가 owner = CommercialBankA로 시작할 수 없는지 궁금할 것입니다. 당사자의 허가 없이는 당사자가 서명자가 될 수 없으며 Iou 템플릿에는 서명자 발급자, 소유자가 명시되어 있으므로 직접 생성할 수 없습니다. 서명자 추가는 일반적으로 잠재적 서명자가 서명자로 추가하는 메서드 (이 경우 IouTransfer_Accept 메서드)를 실행하도록 하여 수행됩니다.

Corda에서는 state가 인스턴스화 되기 전에 계약 클래스에 지정된 대로 상태에 각 서명자가 있어야 함을 요구하여 이를 모델링합니다. 그러면 서명자는 Corda 흐름 중에 트랜잭션에 서명할 수 있습니다.

이제 준비은행(reserve bank)에
issuer = ReserveBank, owner = ReserveBank, value = 1000
인 Iou가 있으므로 newOwner = CommercialBankA 인수를 사용하여 Iou_Transfer 메서드를 실행할 수 있습니다. 이전 Iou 상태가 소비됩니다.

이제 우리는 IouTransfer 상태가 있고 newOwnerIouTransfer_Accept 메서드를 실행할 수 있으며, 그 결과 그를 소유자로 하는 새로운 Iou 상태가 됩니다. 이전 IouTransfer상태가 프로세스에서 사용됩니다. IouTransfer_Accept 메서드(choice)를 실행함으로써 그는 모든 후속 상태에 대한 서명자로서 자신을 인정합니다. 이 경우에는 Iou 유형의 상태입니다. 동일한 결과를 얻을 수 있는 여러 가지 방법이 있으며 위에서 언급 한 단계보다 더 효율적인 방법이 있습니다. 예를 들어 ReserveBankIouTransfer를 직접 생성할 수 있지만 명확성을 위해 단계를 확장했습니다. 앞서 언급 한 단계를 요약 한 다이어그램은 아래에서 찾을 수 있습니다.

이제 Corda 및 DAML에서 상태 및 트랜잭션을 모델링하는 방법을 간략하게 살펴 보았습니다!

이 시리즈의 다음 부분에서는 문서, 개발 용이성 및 테스트 가능성을 포함하여 Corda 대 DAML의 개발자 경험을 검토 할 것입니다.

원문 : Block8 Deep Dive Series: R3’s Corda vs Digital Asset’s DAML
https://www.block8.com/blog/block8-rates-r3s-corda-vs-digital-assets-daml-part-one-overview-of-state-and-transactions