본문 바로가기

옥탑방주인/Hyperledger

Hyperledger Sawtooth - Transaction Processor Tutorial Python (Sawtooth v1.0.2)


Transaction Processor Tutorial Python



본 내용을 설명하기 전에, 이전 글에 있는 XO게임 튜토리얼을 실행하고 오면, 이번 섹션에 대한 이해가 좀 더 수월할 수 있습니다.


Overview


이번 튜토리얼에서는 Sawtooth SDK에 기반된 Python에서의 새로운 Sawtooth transaction family의 생성을 다룬다. 멀티플레잉 게임인 tic-tac-toe(이전에는 XO게임이라 하더니 -_-;)의 배포 버전을 구현하는 transaction handler를 구축해볼 것 이다.


노트 

SDK는 tic-tac-toe의 버전이 구현된 전체를 포함하고 있다. 이번 튜토리얼은 완벽한 구현을 만드는것 보다 이 게임과 관련된 개념을 설명하는데 의미를 둘 것이다. 다양한 언어로 구현되어있는 SDK를 보려면 여기를 클릭해라.


규칙을 포함하는 tic-tac-toe의 일반적인 설명은 Wikipedia에서 찾을 수 있다.


 https://en.wikipedia.org/wiki/Tic-tac-toe


tic-tac-toe transaction family의 모든 부분이 구현된것은 아래에서 확인 할 수 있다.


/project/sawtooth-core/sdk/examples/xo_python/ 



Prerequisites


이 튜토리얼은 installing and Running Sawtooth를 실행해봤으며, 여기에 소개된 개념을 잘 알고있다고 가정하고 진행한다.(이전에 포스팅해 놨음)



The Transaction Processor


Transaction processor에는 2개의 top-level component(processor class와 handler class)가 있다. SDK는 general-purpose processor class를 제공한다. handler class는 application-dependent이고 transaction계열(family)에 대한 비즈니스 로직을 포함하고 있다. 다수의 handlers는 processor class의 instance로 접속될 수 있다.


Handler는 2가지 방법으로 호출된다.


  1. An 'apply' method
  2. Various "metadata" method
메타 데이터는 processor에 handler를 연결시키는데 사용된다. 이것과 관련된 사항은 튜토리얼 끝부분에서 언급하도록 하겠다. handler의 대부분은 'apply'와 helper function로 구성되어 있으므로, 여기서 부터 설명을 시작하겠다.


The apply Method


apply는 'transaction'과 'context' 두개의 인자(argument)를 호출한다. 이 'transaction'인자는 protobuf definition으로부터 생성된 Transaction 클래스의 인스턴스(instance)이다. 마찬가지로, 'context'는 python SDK의 Context 클래스의 인스턴스이다. 


'transcation'은 현재 게임 상태(예를들어; XO게임 보드의 레이아웃과 O,X가 어떻게 생성되는지)에 관한 정보를 'context'에 저장하는동안 실행되는(X,O의 선택 공간 또는 게임 생성) 명령어를 가진다. 


transaction은 validator core에 대해 불투명한 payload bytes와 특정 transaction family도 포함된다. 트랜잭션 validator를 구현할 때 binary serialization 프로토콜은 구현자(implementer)의 몫입니다. 


이 정보에 관해서 어떻게 암호화(encoded)되는지에 관한 것을 얻지 못했다면, 'apply'가 무엇을 필요로 하는지에 관해 생각해 볼 수 있다. 'apply'가 필요로 하는 것은


  1. transaction으로부터 command data를 언패킹 하는것.
  2. 상황 정보(context)로 부터 게임 데이터를 검색(retrieve)하는것
  3. 게임을 플레이 하는 것
  4. 업데이트된 게임 데이터를 저장하는 것.

그래서, 'apply'에 하향식 접근법(top-down approach)은 아래와 같다.


def apply(self, transaction, context):

    signer, game_name, action, space = \

        self._unpack_transaction(transaction)


    board, state, player1, player2 = \

        self._get_state_data(game_name, context)


    updated_game_data = self._play_xo(

        board, state,

        player1, player2,

        signer, action, space

    )


    self._store_game_data(game_name, updated_game_data, context)


세 번째 단계는 실제로 tic-tac-toe와 관련된 유일한 것이다. 다른 세 단계는 모두 데이터 조정(coordination of data)에 관한 것입니다.



Data


노트 

Transaction과 Batches에는 transaction이 어떻게 제작되고 사용되는지에 관해 자세하게 설명되어있다. 이 문서를 읽지 않았다면, 먼저 이 문서를 읽는것을 추천한다.


그렇다면, transaction에서 데이터를 어떻게 얻을 수 있을까? transaction은 header와 payload로 구성되어 있다. header에는 현재 사용자를 식별하는데 사용되는 "signer"를 포함하고 있다. payload는 space(공간을 차지하려 했을때 어느 공간이 비었는지, 안비었는지 알려줌), action('create' ; 게임을 만들고, 'take' 공간을 차지하려하는 행동), 게임의 이름을 암호화하는것을 포함하고 있다. _unpack_transaction 함수는 아래와 같다:


 def _unpack_transaction(self, transaction):

    header = transaction.header

    signer = header.signer


    try:

        game_name, action, space = self._decode_data(transaction.payload)

    except:

        raise InvalidTransaction("Invalid payload serialization")


    return signer, game_name, action, space



transaction payload가 정확히 어떻게 복호화(decoded)되는지를 알기 전에, _get_state_data 함수를 보자. handler에 관한 한, 게임 데이터가 어디에 저장되든 상관없다. 중요한 것은 게임 이름이 주어지면 상태 저장소(state store)가 정확한 게임 데이터를 돌려 줄 수 있다는 것이다(우리의 전체 XO 구현에서는 game data는 Merkle-Radix tree에 저장되어 있다).


def _get_state_data(self, game_name, context):

    game_address = self._make_game_address(game_name)


    state_entries = context.get_state([game_address])


    try:

        return self._decode_data(state_entries[0].data)

    except IndexError:

        return None, None, None, None

    except:

        raise InternalError("Failed to deserialize game data.")



규약에 따라 게임 이름 앞에 몇 가지 상수가 붙은 해시에서 얻은 주소에 게임 데이터를 저장합니다.


def _make_game_address(self, game_name):

    prefix = self._namespace_prefix

    game_name_utf8 = game_name.encode('utf-8')

    return prefix + hashlib.sha512(game_name_utf8).hexdigest()[0:64] 



마지막으로, game data를 저장할 것이다. 이렇게하려면 게임의 업데이트 된 상태를 인코딩하고 원래의 주소로 다시 저장하면됩니다.


def _store_game_data(self, game_name, game_data, context):

    game_address = self._make_game_address(game_name)


    encoded_game_data = self._encode_data(game_data)


    addresses = context.set_state(

        {game_address: encoded_game_data}

    )


    if len(addresses) < 1:

        raise InternalError("State Error") 



그렇다면 데이터를 어떻게 인코딩하고 디코딩해야합니까? binary encoding방식에는 많은 옵션이 있습니다. validator state에 저장된 이진 데이터(binary data)는 처리기의 구현자에게 달려 있습니다. 이 경우 데이터를 간단한 UTF-8 쉼표(comma-separated)로 구분 된 값 문자열로 인코딩 하겠지만보다 정교한 BSON을 사용할 수 있습니다.


def _decode_data(self, data):

    return data.decode().split(',')


def _encode_data(self, data):

    return ','.join(data).encode() 





Implementing Game Play


게임 플레이 기능은 다른 방식으로 구현될 수 있다. 우리가 구현한것에서는, sawtooth-core/sdk/examples/xo_python/sawtooth_xo/processor/handler.py에서 _play_xo 함수가 구현될것을 볼 수 있다. 보드에서 길이 9의 문자열로 표현하기 위해 문자열의 각 문자가 X로 찍은 공간, O로 찍은 공간 또는 빈 공간을 나타낸다.



The XoTransactionHandler Class


이제 남은 일은 XoTransactionHandler class와 metadata를 설정하는 것이다. metadata는 처리 할 수있는 transaction의 종류에 대한 정보를 보내 transaction processor에 validator를 등록하는 데 사용됩니다.


class XoTransactionHandler:

    def __init__(self, namespace_prefix):

        self._namespace_prefix = namespace_prefix


    @property

    def family_name(self):

        return 'xo'


    @property

    def family_versions(self):

        return ['1.0']


    @property

    def encodings(self):

        return ['csv-utf8']


    @property

    def namespaces(self):

        return [self._namespace_prefix]


    def apply(self, transaction, context):

        # ...