Read-Write set 시맨틱

이 문서는 read-write set의 시맨틱에 대한 현재 구현에 대한 상세를 다룹니다.

트랜잭션 시뮬레이션과 read-write set

endorser 에서의 트랜잭션 시뮬레이션 동안 하나의 read-write set이 그 트랜잭션을 위해 준비됩니다. read set 은 시뮬레이션 동안 그 트랜잭션이 읽는 고유한 키들과 (값이 아닌) 그 커밋 버전 넘버들의 하나의 리스트를 담습니다. write set 은 그 트랜잭션이 쓰는 고유한 키(read set 내에 존재하는 키들과 겹칠 수 있긴 하지만)들과 그 값들의 하나의 리스트를 담습니다. delete 마커는 그 트랜잭션이 수행하는 업데이트가 어떤 키를 지우는 것이면 그 키에 대해 (새로운 값이 담길 곳에) 셋팅 됩니다.

뿐만 아니라 그 트랜잭션이 한 키에 여러 번 값을 쓴다면 마지막에 쓰여진 값만 남습니다. 또한 한 트랜잭션이 한 키의 값을 읽으면, 그 트랜잭션이 그를 읽기 전에 값을 업데이트 했다 하더라도 원래 커밋 되어져 있던 상태 내의 값이 리턴됩니다. 바꿔 말하면, Read-your-writes 시맨틱은 지원하지 않습니다.

앞서 말한 바와 같이 키들의 버전은 read set 내에만 기록됩니다; write set은 그냥 고유한 키들과 트랜잭션이 마지막으로 셋팅한 값들의 리스트를 갖습니다.

버전을 구현하기 위해 가능한 다양한 방식이 있습니다. 버전을 매기는 방식의 최소 요구사항은 주어진 하나의 키에 대해 중복되지 않는 ID를 만드는 겁니다. 예를 들면, 버전에 단조롭게 증가하는 숫자를 사용하는 것이 그런 방식이 될 수 있습니다. 현재 구현에서 우리는 한 트랜잭션이 바꾸는 모든 키에 대해서 커밋 중인 그 트랜잭션의 높이를 최신 버전으로 사용하는 블록체인 높이 기반 버전 방식을 사용합니다. 이 방식에서는 한 트랜잭션의 높이를 하나의 튜플 (txNumber는 그 블록 내의 트랜잭션의 높이)로 표현합니다. 이 방식은 증가하는 숫자 방식을 넘어 많은 이점을 갖습니다 - 이는 statedb, 트랜잭션 시뮬레이션과 유효성 검사 같은 다른 컴포넌트에 효율적 디자인을 선택할 수 있도록 합니다.

다음은 가상 트랜잭션의 시뮬레이션으로 준비한 read-write set의 예제 설명입니다. 간단히 하기 위해 설명에서는 버전을 증가하는 숫자로 표현했습니다.

<TxReadWriteSet>
  <NsReadWriteSet name="chaincode1">
    <read-set>
      <read key="K1", version="1">
      <read key="K2", version="1">
    </read-set>
    <write-set>
      <write key="K1", value="V1">
      <write key="K3", value="V2">
      <write key="K4", isDelete="true">
    </write-set>
  </NsReadWriteSet>
<TxReadWriteSet>

추가로, 그 트랜잭션이 시뮬레이션 동안 range query를 실행하면, range query 뿐만 아니라 그 결과들이 read-write set에 query-info 로 추가될 겁니다.

트랜잭션 유효성 검사와 read-write set을 사용한 world state 업데이트

committer 는 read-write set의 read set 일부를 트랜잭션의 유효성을 검사하기 위해 사용하고 read-write set의 write set 일부를 버전과 반영된 키의 값을 업데이트 하는데 사용합니다.

유효성 검사 단계에서는 트랜잭션이 시뮬레이션 됐던 이후로 새로운 블록들로부터 state로 커밋되었던 유효 한 트랜잭션들 뿐만 아니라 같은 블록 내의 유효한 앞 쪽 트랜잭션들까지 고려해서, (시뮬레이션 때부터) 트랜잭션의 read set 내에 있는 각 키의 버전이 같은 키의 현재 버전과 동일하다면 유효 하다고 여깁니다. read-write set이 하나 이상의 query-info를 갖고 있다면 추가 유효성 검사가 이뤄집니다.

추가 유효성 검사는 query-info(들) 내에 기록된 결과의 super range(즉, range들의 union) 내에서 삽입/삭제/업데이트되는 키가 없음을 보장해야 합니다. 바꿔 말하면, 커밋된 상태 상의 유효성 검사 중 어떤 range query(시뮬레이션 동안 실행된 트랜잭션)이든 재실행하면, 그 트랜잭션에 의해 시뮬레이션 때 본 것과 같은 결과가 나와야 합니다. 이 체크는 어떤 트랜잭션이 커밋 중 phantom 항목을 보면, 그 트랜잭션을 유효하지 않음(invalid)으로 표시함을 보장합니다. 이 phantom protection은 range query(즉, 체인코드 내의 GetStateByRange 함수)에 제한되고, 아직 다른 query(즉, 체인코드 내의 GetQueryResult 함수)를 위해 구현되지 않았음에 주의하세요. 다른 query들은 phantom의 위험이 있고, 그래서 애플리케이션이 시뮬레이션과 유효성 검사/커밋 사이 시간의 result set의 안정성을 보장할 수 없는 한, ordering에 보내지 않는 read-only 트랜잭션에만 사용되어야 합니다.

트랜잭션이 유효성 검사를 통과하면, committer는 world state를 업데이트 하기 위해 write set을 사용합니다. 업데이트 단계에서는 write set 내에 있는 각 키에 대해, 같은 키의 world state 내의 각 값이 write set 내에 적힌 값으로 셋팅 됩니다. 그에 더해, world state내의 키의 버전이 최신 버전을 반영하도록 바뀝니다.

시뮬레이션과 유효성 검사 예제

이 섹션에서는 예제 시나리오를 통해 이해를 돕습니다. 이 예제의 목적을 위해 키 k 가 world state 내에 존재함을 (k,ver,val) 튜플로 표현합니다. 여기서, ver 은 값으로 val 을 갖는 키 k 의 최신 버전입니다.

이제, world state의 같은 스냅샷 상에서 모두 시뮬레이션되는 다섯개의 트랜잭션 T1, T2, T3, T4, T5 를 보죠. 다음 조각은 트랜잭션들이 시뮬레이션되는 world state의 스냅샷과 이들 트랜잭션 각각이 읽고 쓰는 동작 순서를 보여줍니다.

World state: (k1,1,v1), (k2,1,v2), (k3,1,v3), (k4,1,v4), (k5,1,v5)
T1 -> Write(k1, v1'), Write(k2, v2')
T2 -> Read(k1), Write(k3, v3')
T3 -> Write(k2, v2'')
T4 -> Write(k2, v2'''), read(k2)
T5 -> Write(k6, v6'), read(k5)

이제 이 트랜잭션들이 T1,…,T5 순서로 (한 블록에도 또는 다른 블록들에도 담길 수 있게) 오더링되었다고 가정합시다.

  1. T1 은 유효성 검사를 통과합니다. 어떠한 Read도 실행하지 않기 때문입니다. world state 내의 키 k1k2(k1,2,v1'), (k2,2,v2') 로 업데이트 됩니다.
  2. T2 는 유효성 검사에 실패합니다. 앞선 트랜잭션 T1 이 수정한 키 k1 을 읽기 때문입니다.
  3. T3 는 유효성 검사를 통과합니다. Read를 실행하지 않기 때문입니다. 이에 더해 world state 내의 키 k2 의 튜플은 (k2,3,v2'') 로 업데이트 됩니다.
  4. T4 는 유효성 검사에 실패합니다. 앞선 트랜잭션 T1 이 수정한 키 k2 를 읽기 때문입니다.
  5. T5 는 유효성 검사를 통과합니다. 앞선 어떤 트랜잭션도 수정하지 않은 키 k5 를 읽기 때문입니다.

주의: Multiple read-write set으로의 트랜잭션은 아직 지원되지 않습니다.