본문 바로가기

Blockchain/Blockchain In Action

[Blockchain In Action] 3장 신뢰와 무결성을 위한 기법

Blockchain In Action 3장.


블록체인 인 액션 책을 읽고 공부한 내용을 기록한 글 입니다.
글에 나와있는 내용과 사진은 모두 블록체인 인 액션에 포함된 내용 혹은 이를 정리한 것 입니다.
문제가 될 시 삭제하겠습니다.

신뢰

신뢰와 무결성은 탈중앙화 시스템에서는 특히 중요하다. 탈중앙화 시스템에서는 블록체인이 중개자의 역할을 대신해서 신뢰 및 중개자의 역할을 해결한다. 

Blockchain in action 책에서는 신뢰 및 무결성의 구성을 두 개의 사분면 차트로 나타낸다.

Blockchain In Action 그림 3.1

신뢰는 시스템에 참여하는 피어 참여자의 신용에 대한 척도다.

블록체인 기반 시스템에서는 참여자의 관련 데이터와 트랜잭션에 관해 확인과 검증을 하고,

이해관계자들의 합의에 따라 적절한 정보를 변조 불가능하게 저장함으로써 신뢰를 확립한다.

확인과 검증을 통해 신뢰를 확립하는데, 스마트 컨트랙트의 개발에서 이 두 가지 용어를 잘 구분하는 것은 중요하다.

확인은 일반적인 규칙에 의해서 이루어지는 것이며, 검증은 특정한 규칙에 관한 것이다. 

확인은 문제 영역의 일반적인 또는 전역적인 요구 조건을 다루는 것이라 생각할 수 있고,

검증은 애플리케이션 또는 데이터의 특정한 조건을 다루는 것이라고 생각할 수 있다.


무결성

블록체인에서 무결성이란 데이터의 보안과 프라이버시, 그리고 트랜잭션의 기밀성을 보장하는 것을 말한다. 

즉, 대상 시스템의 참여자와 그들이 보낸 메세지, 데이터, 오퍼레이션의 정확성에 관한 것이다.

Blockchain In Action 그림 3.1

오른쪽 사분면 차트에서 볼 수 있듯이, 노드에 있는 피어 참여자를 고유하게 식별하는 방법부터 다룬다.

블록체인 어카운트는 참여자를 고유하게 식별하는 간단한 방법이다. 

프라이버시, 보안, 아이덴티티, 기밀성은 무결성의 요소로서, Private-Public 키 쌍 개념을 주로 기반하고 있다. 


투표 유스 케이스

책의 3장에서는 투표에 관한 유스케이스를 다룬다. 주요 행위자와 역할은 다음과 같이 설정된다.

- 의장은 투표자를 등록하며, 자신도 스스로 등록하고 투표할 수 있다.

- 투표자는 투표를 한다.

- 불특정 다수 누구나 투표 과정의 승자나 결과를 요청할 수 있다.

코드의 점진적 개발을 위해 책에서는 4개의 단계로 나누었다.

  1. BallotV1: 스마트 컨트랙트의 데이터 구조를 정의하고 테스트
  2. BallotV2: constructor와 투표 상태를 변화시키기 위한 함수를 추가
  3. BallotV3: 스마트 컨트랙트의 다른 함수와 신뢰 구축을 위한 솔리디티 기능을 보여주기 위한 수정자 추가
  4. BallotV4: 신뢰 요소인 require(), revert(), assert()와 함수 접근 수정자를 추가

BallotV1의 코드

pragma solidity 0.6.0;
contract BallotV1 {

    struct Voter {
        uint weight;
        bool voted;
        uint vote;
    }

    struct Proposal {
        uint voteCount;
    }

    address chairperson;
    mapping(address => Voter) voters;
    Proposal[] proposals;

    enum Phase {Init, Regs, Vote, Done}
    Phase public state = Phase.Init;
}

Voter type은 투표자의 상세 정보를 담고 있다.
Proposal type은 제안의 상세 정보를 담고 있는데, 현재는 voteCount만 담고 있다.
그 밑에서는 투표자의 address를 투표자의 상세 정보로 mapping 하고 있다.
Phase는 투표의 여러 단계 (0, 1, 2, 3) 를 나타내고, init 단계로 상태가 초기화된다.

이 파일을 deploy 하면 public 으로 선언을 해두었기 때문에 유저가 state 기능을 테스트할 수 있고, 현재 상태가 0임을 확인할 수 있다.

만일 state 선언문에서 public을 지우면 가시성 수정자가 없기 때문에 인터페이스에서 state 버튼이 사라질 것이다. 

 이와 같은 점진적 개발을 통해 우리는 모든 데이터 정의에 구문적 에러가 없다는 것을 확인할 수 있다.

스마트 컨트랙트의 상태 변화는 시간과 여러 조건에 의해 변화해야 하는 경우가 많다. 그 예시는 다음과 같다.

- 투표 유스 케이스에서, 등록은 투표 전에 마쳐야 하고, 특정한 종료 시간 전에 이루어져야 한다.

- 투표 과정을 위한 함수는 특정한 순서에 따라 실행한다.

- 투표는 오직 정해진 기간만 가능하다. 

- 투표가 종료되어야만 승자를 판단할 수 있다.

Blockchain In Action 그림 3.5

투표 과정을 나타내는 네 개의 단계, Init, Regs, Vote, Done을 정의한다. 

시스템은 우선 초기화된 Init 단계를 거쳐 Regs 단계로 전이되는데, 여기서 등록이 일어난다. 10일간의 등록 이후, 시스템은 Vote 단계로 이동하는데, 여기서 하루 동안 투표가 일어난다. 마지막으로 Done으로 넘어가서 승자를 판별한다. 

이 케이스에서 상태 변화는 시간을 기반으로 일어난다. 

BallotV2의 코드

pragma solidity >=0.4.22 <=0.6.0;
contract BallotV2 {

    struct Voter {                     
        uint weight;
        bool voted;
        uint vote;
    }
    struct Proposal {                  
        uint voteCount;
    }

    address chairperson;
    mapping(address => Voter) voters;  
    Proposal[] proposals;

    enum Phase {Init,Regs, Vote, Done}
    // 단계는 오직 0, 1, 2, 3만 가질 수 있고 다른 값은 전부 무효
    Phase public state = Phase.Init;
    
    
    // constructor 는 배포자로서 의장을 설정한다.
    constructor (uint numProposals) public  {
        chairperson = msg.sender;
        voters[chairperson].weight = 2; 
        for (uint prop = 0; prop < numProposals; prop ++)
            proposals.push(Proposal(0));
        
    }
    
    // 상태 변화 함수
    function changeState(Phase x) public {
        // 오직 의장만이 상태를 바꿀 수 있다.
        if (msg.sender != chairperson) {revert();}
        // 상태는 0 1 2 3 순서로 변경할 것이다. 그렇지 않은 경우 되돌린다.
        if (x < state ) revert();
        state = x;
    }
    
    
}

솔리디티는 신뢰 중개를 위한 요구 조건을 다룰 여러 가지 언어적 기능과 함수를 제공한다.

modifier는 확인해야할 접근 통제 규칙을 명시하고 신뢰와 프라이버시를 구축하기 위해 데이터와 함수에 대한 통제권을 누가 갖는 지를 관리한다. 이러한 수정자는 public(가시성 수정자)와 구분해서 액세스 수정자라고 불린다. 

require 선언은 파라미터로 전달된 조건을 검증하고 만일 실패할 경우 함수를 중단한다. 이러한 기능은 파라미터의 일반적 검증을 위해 공통으로 사용한다.

revert 선언은 트랜잭션을 중단시키고 블록체인에 기록되는 것을 막아준다. 보통은 수정자 정의에 사용된다.

assert 선언문은 변수의 조건이나 함수의 실행 과정에서 데이터를 검증하는데, 만일 실패할 경우 트랜잭션을 되돌린다. 이 기능은 어떤 조건을 만족시키지 못해서 예외 상태가 일어나지 않도록 하기 위해서 사용한다. 예를 들어, 크루즈 여행 중인 바다 한 가운데서 인원수를 검증하거나, 은행에 잔고가 충분하지 않을 때 청구서를 지급하지 못하도록 하는 경우다. 

수정자 정의 구문과 예시

modifier name_of_modifier(parameter) {
	require { conditions_to_be_checked } ;
    _;
}

modifier validPhase(Phase rePhase) {
	require (state == rePhase);
    _;
}

수정자를 포함한 컨트랙트 다이어그램

Blockchain In Action 그림 3.6

수정자를 포함하는 BallotV3 코드

pragma solidity >=0.4.22 <=0.6.0;
contract BallotV3 {

    struct Voter {                     
        uint weight;
        bool voted;
        uint vote;
    }
    struct Proposal {                  
        uint voteCount;
    }

    address chairperson;
    mapping(address => Voter) voters;  
    Proposal[] proposals;

    enum Phase {Init,Regs, Vote, Done}  
    Phase public state = Phase.Done; 
    
       //modifiers
   modifier validPhase(Phase reqPhase) 
    { require(state == reqPhase); 
      _; 
    } 
    
     
    
    constructor (uint numProposals) public  {
        chairperson = msg.sender;
        voters[chairperson].weight = 2; // weight 2 for testing purposes
        //proposals.length = numProposals; -- before 0.6.0
        for (uint prop = 0; prop < numProposals; prop ++)
            proposals.push(Proposal(0));
        state = Phase.Regs;
    
    }
       
    function changeState(Phase x) public {
        if (msg.sender != chairperson) {revert();}
        if (x < state ) revert();
        state = x;
    }
    
    
    function register(address voter) public validPhase(Phase.Regs) {
        if (msg.sender != chairperson || voters[voter].voted) revert(); 
        voters[voter].weight = 1;
        voters[voter].voted = false;
          
    }

   
    function vote(uint toProposal) public validPhase(Phase.Vote)  {
       
        Voter memory sender = voters[msg.sender];
        if (sender.voted || toProposal >= proposals.length) return; 
        sender.voted = true;
        sender.vote = toProposal;   
        proposals[toProposal].voteCount += sender.weight;
            
        
    }

    function reqWinner() public validPhase(Phase.Done) view returns (uint winningProposal) {
       
        uint winningVoteCount = 0;
        for (uint prop = 0; prop < proposals.length; prop++) 
            if (proposals[prop].voteCount > winningVoteCount) {
                winningVoteCount = proposals[prop].voteCount;
                winningProposal = prop;
            }
       
    }
}

여기까지 코딩을 하면 수정자 기능을 이용해 어떻게 규칙을 설정할 지에 대해 대충 마무리가 되었으나,

함수를 실행하는데 다수의 규칙이 필요한 경우가 있을 수 있다. register() 함수에서 오직 의장만이 다른 투표자를 등록할 수 있다.

이 규칙을 require, revert, onlyChair 수정자로 강제할 수 있다.

또한, assert() 내장 함수를 이용해서 이기기 위해서는 과반수의 표가 필요하다고 강제할 수 있다. 

이러한 모든 신뢰 규칙을 포함한 코드는 다음과 같다.

모든 신뢰 규칙을 코딩한 상태 BallotV4 코드

pragma solidity >=0.4.22 <=0.6.0;
contract BallotV4 {

    struct Voter {                     
        uint weight;
        bool voted;
        uint vote;
    }
    struct Proposal {                  
        uint voteCount;
    }

    address chairperson;
    mapping(address => Voter) voters;  
    Proposal[] proposals;

    enum Phase {Init,Regs, Vote, Done}  
    Phase public state = Phase.Done; 
    
       //modifiers
   modifier validPhase(Phase reqPhase) 
    { require(state == reqPhase); 
      _; 
    } 
    
    modifier onlyChair() 
     {require(msg.sender == chairperson);
      _;
     }

    
    constructor (uint numProposals) public  {
        chairperson = msg.sender;
        voters[chairperson].weight = 2; // weight 2 for testing purposes
        //proposals.length = numProposals; -- before 0.6.0
        for (uint prop = 0; prop < numProposals; prop ++)
            proposals.push(Proposal(0));
        state = Phase.Regs;
    }
    
     function changeState(Phase x) onlyChair public {
        
        require (x > state );
       
        state = x;
     }
    
    function register(address voter) public validPhase(Phase.Regs) onlyChair {
       
        require (! voters[voter].voted);
        voters[voter].weight = 1;
        
    }

   
    function vote(uint toProposal) public validPhase(Phase.Vote)  {
      
        Voter memory sender = voters[msg.sender];
        
        require (!sender.voted); 
        require (toProposal < proposals.length); 
        
        sender.voted = true;
        sender.vote = toProposal;   
        proposals[toProposal].voteCount += sender.weight;
    }

    function reqWinner() public validPhase(Phase.Done) view returns (uint winningProposal) {
       
        uint winningVoteCount = 0;
        for (uint prop = 0; prop < proposals.length; prop++) 
            if (proposals[prop].voteCount > winningVoteCount) {
                winningVoteCount = proposals[prop].voteCount;
                winningProposal = prop;
            }
       assert(winningVoteCount>=3);
    }
}