RLP 이해하기

이더리움 네트워크에서 노드에 데이터 구조를 저장하거나, 혹은 노드끼리 데이터 구조를 주고 받으려면 통일된 형식이 필요하다. 그리고 이 통일된 형식을 만드는 것을 직렬화(serialization)라고 하고, 만들어진 통일된 형식을 바이트 스트림이라고 한다.

RLP는 이더리움 네트워크에서 쓰이는 직렬화(serialization) 기법이다. 바이트 스트림은 최소 단위를 바이트로 하는 데이터 묶음이다. 데이터를 파일에 저장하거나 네트워크에서 전송하기 위해서는 바이트 스트림 형식으로 변환되어야 한다.

RLP는 데이터를 저장하거나 전송하는데 필요한 통일된 포맷을 제공한다. 데이터는 RLP로 변환되어 트랜잭션 전송, 블록 state 및 receipt 저장, DB 저장 등에 사용된다.

이 글에서는 RLP를 1)RLP 인코딩 2)RLP 디코딩으로 나눠서 이해해보겠다. RLP의 기본 단위는 아이템(item)이다. 아이템은 크게 string(i.e byte array) 아이템과 배열 아이템으로 나뉘어진다.

예를 들어, “cat”은 하나의 string 아이템이고, [“cat”, “horse”]는 하나의 배열 아이템과 두 개의 string 아이템, 즉 3개의 아이템으로 볼 수 있다.

1> RLP 인코딩

우선, string을 어떻게 RLP로 변환하는지에 대해서 알아보자.

1) 만약 단일 바이트가 [0x00, 0x7f] 사이의 값이라면, 그 바이트는 자체로 RLP 인코딩이 된다.

ex) rlp.encode(0x3) # 리턴값 b’\x03'

rlp.encode(0x7f) # 리턴값 b’\x7f’

2) 만약 문자열이 0–55 bytes 사이의 길이라면,

싱글 바이트인 0x80에 문자열의 길이를 더하고, 문자열 바이트를 붙인다.

따라서 첫번째 바이트는 [0x80, 0xb7] 사이의 값을 가진다.

ex) The string “dog” = [ 0x83, ‘d’, ‘o’, ‘g’ ]

-> 0x80 + 3(문자열 길이) + dog = b’\x83dog

3) 만약 문자열의 길이가 55 bytes를 넘어간다면,

0xb7에 길이값의 바이트값을 더하고, 길이의 바이트값을 붙인 뒤에,

문자열의 바이트를 붙인다.

ex) rlp.encode(“a”*1024) = [0xb9, 0x04, 0x00, ‘a’, …., ’a’(1024개)]

1024를 바이트로 표현하면, <0x04> <0x00> 2바이트.

<0xb7 + 0x02>뒤에 0x04 0x00(1024의 hex값)을 붙이면

→ 0xb9 0x04 0x00

→그 뒤에 a를 1024개 붙이면 끝

→<b’\xb9\x04\x00aaaa….a가 1024개>

다음은 배열을 어떻게 RLP로 변환하는지에 대해서 알아보자.

4) 만약 배열의 모든 아이템들이 RLP 인코딩된 값의 길이가 55 bytes보다 작다면,

0xc0에 아이템들을 각각 RLP 인코딩한 값의 길이를 더하고,

각각의 아이템의 인코딩한 값들을 붙인다.

따라서 첫 바이트의 값은 [0xc0, 0xf7] 사이의 값을 가진다.

ex) rlp.encode([“ab”]) => [0xc3, 0x82, a, b]

> “ab”를 RLP인코딩하면, 0x82 a b 가 된다. 길이는 3.

0xc0+3 = 0xc3.

최종 값은 <0xc3 0x82 a b> → b’\xc3\x82ab’

ex) rlp.encode([“ab”,”dc”]) => [ 0xc6, 0x82, a, b, 0x82, d, c ]

> “ab”를 인코딩하면 0x82 a b, “dc”를 인코딩하면 0x82 d c, 둘의 길이를 합치면 6

0xc0+6 = 0xc6

최종 값은 <0xc6 0x82 a b 0x82 d c> → b’\xc6\x82ab\x82dc’’

5) 만약 배열의 모든 아이템들이 RLP 인코딩된 값들의 길이가 55 bytes보다 크다면,

0xf7에 아이템들을 각각 RLP인코딩한 값들의 길이를 표현한 값의 길이를 더하고,

아이템들의 길이값의 합을 붙이고, 각각의 아이템의 RLP인코딩한 값들을 붙인다.

따라서 첫 바이트의 값의 범위는 [0xc7, 0xff]가 된다.

ex) rlp.encode([“a”*50, “a”*50]) => [0xf8, f, 0xb2a….a, 0xb2a…..a]

> “a”*50를 인코딩하면 0x80 + 0x32(50개이므로)= 0xb2 에다 a를 50개 붙이면 된다.

(1–2) 참조)

0xb2a…a50개, 총 51자리.

> 아이템 2개의 rlp 인코딩 길이는 51*2 = 102, 헥스로 바꾸면 0x66, 1자리.

> 따라서 0xf7+1 = 0xf8,

여기에 아이템들의 길이값인 102 → 0x66(=f).

뒤에 아이템2개 붙임 → b’\xf8f\xb8da…a50개0xb2a…a50개’

추가적인 예시를 통해서 실제 RLP인코딩을 좀 더 이해할 수 있다.

Examples

The string “dog” = [ 0x83, ‘d’, ‘o’, ‘g’ ]
The list [ “cat”, “dog” ] = [ 0xc8, 0x83, ‘c’, ‘a’, ‘t’, 0x83, ‘d’, ‘o’, ‘g’ ]
The empty string (‘null’) = [ 0x80 ]
The empty list = [ 0xc0 ]
The integer 0 = [ 0x80 ]
The encoded integer 0 (‘\x00’) = [ 0x00 ]
The encoded integer 15 (‘\x0f’) = [ 0x0f ]
The encoded integer 1024 (‘\x04\x00’) = [ 0x82, 0x04, 0x00 ]

2> RLP 디코딩

RLP 디코딩은 RLP 인코딩의 역과정이다. RLP 디코딩의 과정은 다음과 같이 진행된다.

1. 입력 데이타의 첫번째 바이트에 따라, 데이타의 타입과 아이템의 갯수를 파악합니다.

2. 데이타의 유형 및 오프셋에 따라 데이타를 디코딩합니다.

3. 나머지 입력데이타를 계속해서 디코딩합니다.

1. 첫번째 바이트가 [0x00, 0x7f] 사이의 값이면 그값 자체가 문자 데이터입니다. [인코딩 규칙 1)]

2. 첫번째 바이트가 [0x80, 0xb7] 사이의 값이면 문자열이고, 첫번째 바이트에서 0x80을 뺀 값이 문자열의 길이입니다. [인코딩 규칙 2)]

3. 첫번째 바이트가 [0xb8, 0xbf] 사이의 값이면, 첫번째 바이트에서 0xb7을 뺀값이 바이트로 표현된 RLP 아이템의 개수가 되고, 이어서 그 개수만큼 문자가 이어집니다. [인코딩 규칙 3)]

4. 첫번째 바이트가 [0xc0, 0xf7] 사이의 값이면, 배열을 의미하고, 첫번째 바이트에서 0xc0를 뺀 값이 배열의 갯수를 의미합니다. [인코딩 규칙 4)]

5. 첫번째 바이트가 [0xf8, 0xff] 사이의 값이면, 배열을 의미하고, 첫번째 바이트에서 0xf7을 뺀값이 아이템의 갯수이고, 이어서 RLP 인코딩되어 연결된 데이터가 첫번째 바이트 이후에 이어집니다. [인코딩 규칙 5)]

참고 자료:
https://docs.google.com/presentation/d/1nSoRv4hCmona_N1VENZdZ_POVV-LZpxOSNBntareZuA/edit#slide=id.g3597ea7b61_0_98