본문으로 건너뛰기

TL-B 유형

고급 레벨

이 정보는 매우 낮은 수준의 정보로 초보자에게는 이해하기 어려울 수 있습니다. 나중에 다시 읽어보시기 바랍니다.

이 섹션에서는 복잡하고 비전형적인 타입 언어 바이너리(TL-B) 구조를 분석합니다. 시작하려면 먼저 [이 문서](/개발/데이터 형식/tl-b-language)를 읽고 이 주제에 익숙해지는 것이 좋습니다.

tlb structure

둘 중 하나

left$0 {X:Type} {Y:Type} value:X = Either X Y;
right$1 {X:Type} {Y:Type} value:Y = Either X Y;

두 가지 결과 유형 중 하나가 가능한 경우 둘 중 하나 유형이 사용됩니다. 이 경우 유형 선택은 표시된 접두사 비트에 따라 달라집니다. 접두사 비트가 0이면 왼쪽 유형이 직렬화되고, 접두사 비트가 1이면 오른쪽 유형이 직렬화됩니다.

예를 들어 메시지를 직렬화할 때 본문이 메인 셀의 일부이거나 다른 셀에 연결되어 있을 때 사용됩니다.

어쩌면

nothing$0 {X:Type} = Maybe X;
just$1 {X:Type} value:X = Maybe X;

Maybe 타입은 선택적 값과 함께 사용됩니다. 이러한 경우 첫 번째 비트가 0이면 값 자체가 직렬화되지 않고(실제로는 건너뛰고), 값이 1이면 직렬화됩니다.

둘 다

pair$_ {X:Type} {Y:Type} first:X second:Y = Both X Y;

양쪽 유형 변형은 일반 쌍과 함께만 사용되며, 두 유형이 조건 없이 차례로 직렬화됩니다.

Unary

단항 함수 유형은 일반적으로 hml_short와 같은 구조에서 동적 크기 조정에 사용됩니다.

Unary는 두 가지 주요 옵션을 제공합니다:

unary_zero$0 = Unary ~0;
unary_succ$1 {n:#} x:(Unary ~n) = Unary ~(n + 1);

단항 직렬화

일반적으로 unary_zero 변형을 사용하는 것은 매우 간단합니다. 첫 번째 비트가 0이면 전체 단항 역직렬화 결과는 0이 됩니다.

즉, unary_succ 변수는 재귀적으로 로드되고 ~(n + 1)의 값을 갖기 때문에 더 복잡합니다. 즉, unary_zero에 도달할 때까지 순차적으로 자신을 호출합니다. 즉, 원하는 값은 연속된 단위 수와 같을 것입니다.

예를 들어, 비트 문자열 110의 직렬화를 분석해 보겠습니다.

호출 체인은 다음과 같습니다:

unary_succ$1 -> unary_succ$1 -> unary_zero$0

유니타리_제로`에 도달하면 재귀 함수 호출과 유사하게 직렬화된 비트 문자열의 끝에 값이 반환됩니다.

이제 결과를 더 명확하게 이해하기 위해 다음과 같이 표시되는 반환 값 경로를 검색해 보겠습니다:

0 -> ~(0 + 1) -> ~(1 + 1) -> 2``, 즉 110단항 2`로 직렬화했다는 뜻입니다.

단항 역직렬화

Foo` 유형이 있다고 가정합니다:

foo$_  u:(Unary 2) = Foo;

위에서 말한 대로 Foo는 다음과 같이 역직렬화됩니다:



foo u:(unary_succ x:(unary_succ x:(unnary_zero)))

해시맵

해시맵 컴플렉스 타입은 펀씨 스마트 컨트랙트 코드(dict)의 딕셔너리를 저장하는 데 사용됩니다.

다음 TL-B 구조는 키 길이가 고정된 해시맵을 직렬화하는 데 사용됩니다:

hm_edge#_ {n:#} {X:Type} {l:#} {m:#} label:(HmLabel ~l n) 
{n = (~m) + l} node:(HashmapNode m X) = Hashmap n X;

hmn_leaf#_ {X:Type} value:X = HashmapNode 0 X;
hmn_fork#_ {n:#} {X:Type} left:^(Hashmap n X)
right:^(Hashmap n X) = HashmapNode (n + 1) X;

hml_short$0 {m:#} {n:#} len:(Unary ~n) {n <= m} s:(n * Bit) = HmLabel ~n m;
hml_long$10 {m:#} n:(#<= m) s:(n * Bit) = HmLabel ~n m;
hml_same$11 {m:#} v:Bit n:(#<= m) = HmLabel ~n m;

unary_zero$0 = Unary ~0;
unary_succ$1 {n:#} x:(Unary ~n) = Unary ~(n + 1);

hme_empty$0 {n:#} {X:Type} = HashmapE n X;
hme_root$1 {n:#} {X:Type} root:^(Hashmap n X) = HashmapE n X;

즉, 루트 구조는 HashmapE와 그 두 가지 상태(hme_empty 또는 hme_root 포함) 중 하나를 사용합니다.

해시맵 구문 분석 예제

이진 형식으로 주어진 다음 셀을 예로 들어보겠습니다.

1[1] -> {
2[00] -> {
7[1001000] -> {
25[1010000010000001100001001],
25[1010000010000000001101111]
},
28[1011100000000000001100001001]
}
}

이 셀은 HashmapE 구조 유형을 사용하며 키 크기는 8비트이고 값은 uint16 숫자 프레임워크(HashmapE 8 uint16)를 사용합니다. HashmapE는 3개의 고유한 키 유형을 사용합니다:

1 = 777
17 = 111
128 = 777

이 해시맵을 구문 분석하려면 hme_empty 또는 hme_root 중 어떤 구조 유형을 사용할지 미리 알아야 합니다. 이는 올바른 접두사를 식별함으로써 결정됩니다. hme 비어 있는 변형은 비트 0(hme_empty$0)을 사용하는 반면, hme 루트는 비트 1(hme_root$1)을 사용합니다. 첫 번째 비트를 읽은 후 1(1[1])과 같다는 것을 확인하여 hme_root 변형임을 의미합니다.

이제 구조 변수를 알려진 값으로 채워보겠습니다. hme_root$1 {n:#} {X:Type} root:^(해시맵 8 uint16) = 해시맵E 8 uint16; 초기 결과는 다음과 같습니다.

여기서 1비트 접두사는 이미 읽었지만 {} 안에는 읽을 필요가 없는 조건을 나타냅니다.{n:#}조건은 n이 임의의 uint32 숫자라는 것을 의미하며,{X:Type}`은 X가 모든 유형을 사용할 수 있다는 것을 의미합니다.

다음으로 읽어야 하는 부분은 root:^(해시맵 8 uint16)이며, ^ 기호는 로드해야 하는 링크를 나타냅니다.

2[00] -> {
7[1001000] -> {
25[1010000010000001100001001],
25[1010000010000000001101111]
},
28[1011100000000000001100001001]
}

분기 구문 분석 시작

스키마에 따르면, 이것은 올바른 '해시맵 8 uint16' 구조입니다. 다음으로, 알려진 값으로 채우고 다음과 같은 결과를 얻습니다:

hm_edge#_ {n:#} {X:Type} {l:#} {m:#} label:(HmLabel ~l 8) 
{8 = (~m) + l} node:(HashmapNode m uint16) = Hashmap 8 uint16;

위와 같이 조건 변수 {l:#}{m:#}가 등장했지만 두 변수의 값은 알 수 없습니다. 또한 해당 라벨을 읽으면 n{n = (~m) + l} 방정식에 포함된다는 것을 알 수 있으며, 이 경우 lm을 계산하면 ` 부호가 결과 값인 ~를 알려줍니다.

l의 값을 결정하려면 label:(HmLabel ~l uint16)시퀀스를 로드해야 합니다. 아래 그림과 같이HmLabel`에는 3가지 기본 구조 옵션이 있습니다:

hml_short$0 {m:#} {n:#} len:(Unary ~n) {n <= m} s:(n * Bit) = HmLabel ~n m;
hml_long$10 {m:#} n:(#<= m) s:(n * Bit) = HmLabel ~n m;
hml_same$11 {m:#} v:Bit n:(#<= m) = HmLabel ~n m;

각 옵션은 해당 접두사에 의해 결정됩니다. 현재 루트 셀은 2개의 0비트로 구성되어 있으며, 이는 다음과 같이 표시됩니다: (2[00]). 따라서 유일한 논리적 옵션은 0으로 시작하는 접두사를 사용하는 hml_short$0입니다.

hml_short`를 알려진 값으로 채웁니다:

hml_short$0 {m:#} {n:#} len:(Unary ~n) {n <= 8} s:(n * Bit) = HmLabel ~n 8

이 경우 n의 값은 알 수 없지만 ~ 문자가 있으므로 계산할 수 있습니다. 이를 위해 len:(Unary ~n), 여기에서 단항에 대해 자세히 알아보기를 로드합니다.

이 경우 2[00]로 시작했지만 HmLabel 유형을 정의한 후에도 두 비트 중 하나만 존재합니다.

따라서 이를 로드하면 값이 0이므로 분명히 unary_zero$0 변형을 사용한다는 것을 알 수 있습니다. 즉, HmLabel 변형을 사용하는 n 값은 0입니다.

다음으로 계산된 n 값을 사용하여 hml_short 변형 시퀀스를 완성해야 합니다:

hml_short$0 {m:#} {n:#} len:0 {n <= 8} s:(0 * Bit) = HmLabel 0 8

s = 0으로 표시된 빈 HmLabel이 있으므로 다운로드할 항목이 없습니다.

다음으로, 다음과 같이 계산된 l 값으로 구조를 보완합니다:

hm_edge#_ {n:#} {X:Type} {l:0} {m:#} label:(HmLabel 0 8) 
{8 = (~m) + 0} node:(HashmapNode m uint16) = Hashmap 8 uint16;

이제 l의 값을 계산했으므로 n = (~m) + 0, 즉 m = n - 0, m = n = 8이라는 방정식을 사용하여 m을 계산할 수도 있습니다.

알 수 없는 값을 모두 결정한 후 이제 node:(HashmapNode 8 uint16)을 로드할 수 있습니다.

해시맵 노드에 관해서는 옵션이 있습니다:

hmn_leaf#_ {X:Type} value:X = HashmapNode 0 X;
hmn_fork#_ {n:#} {X:Type} left:^(Hashmap n X)
right:^(Hashmap n X) = HashmapNode (n + 1) X;

이 경우 접두사를 사용하지 않고 매개변수를 사용하여 옵션을 결정합니다. 즉, n = 0이면 올바른 최종 결과는 hmn_leaf 또는 hmn_fork가 됩니다. 이 예제에서 결과는 n = 8(hmn_fork 변형)입니다. 여기서는 hmn_fork 변형을 사용하여 알려진 값을 채웁니다:

hmn_fork#_ {n:#} {X:uint16} left:^(Hashmap n uint16) 
right:^(Hashmap n uint16) = HashmapNode (n + 1) uint16;

알려진 값을 입력한 후 '해시맵노드 (n + 1) uint16'을 계산해야 합니다. 즉, n의 결과 값은 매개변수인 8과 같아야 합니다. n의 로컬 값을 계산하려면 다음 공식을 사용하여 계산해야 합니다: n = (n_local + 1)->n_local = (n - 1)->n_local = (8 - 1)->n_local = 7`.

hmn_fork#_ {n:#} {X:uint16} left:^(Hashmap 7 uint16) 
right:^(Hashmap 7 uint16) = HashmapNode (7 + 1) uint16;

이제 위의 공식이 필요하다는 것을 알았으므로 최종 결과를 얻는 것은 간단합니다. 다음으로 왼쪽과 오른쪽 브랜치를 로드하고 각 후속 브랜치에 대해 이 과정이 반복됩니다.

로드된 해시맵 값 분석하기

이전 예제에 이어서 (딕셔너리 값의 경우) 브랜치를 로드하는 프로세스가 어떻게 작동하는지 살펴 보겠습니다. 즉, 28[1011100000000000001100001001]를 예로 들어 보겠습니다.

최종 결과는 다시 'hm_edge'가 되고 다음 단계는 다음과 같이 올바른 알려진 값으로 시퀀스를 채우는 것입니다:

hm_edge#_ {n:#} {X:Type} {l:#} {m:#} label:(HmLabel ~l 7) 
{7 = (~m) + l} node:(HashmapNode m uint16) = Hashmap 7 uint16;

다음으로 접두사가 10이므로 HmLabel 변형을 사용하여 HmLabel 응답을 로드합니다.

hml_long$10 {m:#} n:(#<= m) s:(n * Bit) = HmLabel ~n m;

이제 순서를 채워 보겠습니다:

hml_long$10 {m:#} n:(#<= 7) s:(n * Bit) = HmLabel ~n 7;

새로운 구조인 n:(#<= 7)은 숫자 7에 해당하는 크기 값을 명확하게 나타내며, 실제로는 숫자 + 1의 로그2입니다. 하지만 간단하게 숫자 7을 쓰는 데 필요한 비트 수를 세어볼 수 있습니다. 이와 관련해서 숫자 7을 2진법으로 표현하면 111이므로 3비트가 필요하며, 이는 n = 3의 값을 의미합니다.

hml_long$10 {m:#} n:(## 3) s:(n * Bit) = HmLabel ~n 7;

다음으로 n을 시퀀스에 로드하면 최종 결과는 111이 되며, 위에서 언급한 것처럼 공교롭게도 7이 됩니다. 다음으로 s를 7비트인 0000000 시퀀스에 로드합니다. s`는 키의 일부라는 점을 기억하세요.

다음으로 시퀀스의 맨 위로 돌아가서 결과 l을 채웁니다:

hm_edge#_ {n:#} {X:Type} {l:#} {m:#} label:(HmLabel 7 7) 
{7 = (~m) + 7} node:(HashmapNode m uint16) = Hashmap 7 uint16;

그런 다음 m의 값인 m = 7 - 7, 따라서 m = 0의 값을 계산합니다. 값이 m = 0이므로 이 구조는 해시맵 노드와 함께 사용하기에 완벽합니다:

hmn_leaf#_ {X:Type} value:X = HashmapNode 0 X;

다음으로 uint16 유형으로 대체하고 값을 로드합니다. 십진수 형식의 0000001100001001의 나머지 16비트는 777이므로 우리의 값은 777입니다.

이제 키를 복원하려면 이전에 계산한 키의 모든 부분의 정렬된 목록을 결합해야 합니다. 두 개의 관련 키 부분은 각각 어떤 타입 분기를 사용했는지에 따라 하나의 비트로 결합됩니다. 오른쪽 분기의 경우 '1' 비트가 추가되고 왼쪽 분기의 경우 '0' 비트가 추가됩니다. 위에 전체 HmLabel이 존재하면 해당 비트가 키에 추가됩니다.

구체적으로 이 경우, 오른쪽 분기에서 값을 얻었으므로 HmLabel 0000000에서 7비트를 가져오고 0의 시퀀스 앞에 '1' 비트를 추가합니다. 최종 결과는 총 8비트 또는 10000000이며, 이는 키 값이 128과 같음을 의미합니다.

기타 해시맵 유형

해시맵과 표준화된 해시맵 유형을 로드하는 방법에 대해 설명했으니 이제 추가 해시맵 유형이 어떻게 작동하는지 설명해 보겠습니다.

해시맵AugE

ahm_edge#_ {n:#} {X:Type} {Y:Type} {l:#} {m:#} 
label:(HmLabel ~l n) {n = (~m) + l}
node:(HashmapAugNode m X Y) = HashmapAug n X Y;

ahmn_leaf#_ {X:Type} {Y:Type} extra:Y value:X = HashmapAugNode 0 X Y;

ahmn_fork#_ {n:#} {X:Type} {Y:Type} left:^(HashmapAug n X Y)
right:^(HashmapAug n X Y) extra:Y = HashmapAugNode (n + 1) X Y;

ahme_empty$0 {n:#} {X:Type} {Y:Type} extra:Y
= HashmapAugE n X Y;

ahme_root$1 {n:#} {X:Type} {Y:Type} root:^(HashmapAug n X Y)
extra:Y = HashmapAugE n X Y;

해시맵AugE와 일반 해시맵의 주요 차이점은 각 노드에 extra:Y` 필드가 있다는 것입니다(값이 있는 리프뿐만 아니라).

PfxHashmap

phm_edge#_ {n:#} {X:Type} {l:#} {m:#} label:(HmLabel ~l n) 
{n = (~m) + l} node:(PfxHashmapNode m X)
= PfxHashmap n X;

phmn_leaf$0 {n:#} {X:Type} value:X = PfxHashmapNode n X;
phmn_fork$1 {n:#} {X:Type} left:^(PfxHashmap n X)
right:^(PfxHashmap n X) = PfxHashmapNode (n + 1) X;

phme_empty$0 {n:#} {X:Type} = PfxHashmapE n X;
phme_root$1 {n:#} {X:Type} root:^(PfxHashmap n X)
= PfxHashmapE n X;

PfxHashmap과 일반 해시맵의 주요 차이점은 phmn_leaf$0phmn_fork$1 노드의 존재로 인해 서로 다른 키 길이를 저장할 수 있다는 점입니다.

VarHashmap

vhm_edge#_ {n:#} {X:Type} {l:#} {m:#} label:(HmLabel ~l n) 
{n = (~m) + l} node:(VarHashmapNode m X)
= VarHashmap n X;
vhmn_leaf$00 {n:#} {X:Type} value:X = VarHashmapNode n X;
vhmn_fork$01 {n:#} {X:Type} left:^(VarHashmap n X)
right:^(VarHashmap n X) value:(Maybe X)
= VarHashmapNode (n + 1) X;
vhmn_cont$1 {n:#} {X:Type} branch:Bit child:^(VarHashmap n X)
value:X = VarHashmapNode (n + 1) X;

// nothing$0 {X:Type} = Maybe X;
// just$1 {X:Type} value:X = Maybe X;

vhme_empty$0 {n:#} {X:Type} = VarHashmapE n X;
vhme_root$1 {n:#} {X:Type} root:^(VarHashmap n X)
= VarHashmapE n X;

VarHashmap과 일반 해시맵의 주요 차이점은 vhmn_leaf$00vhmn_fork$01 노드가 있기 때문에 서로 다른 키 길이를 저장할 수 있다는 점입니다. 또한 VarHashmapvhmn_cont$1을 희생하여 공통 값 접두사(자식 맵)를 형성할 수 있습니다.

빈트리

bta_leaf$0 {X:Type} {Y:Type} extra:Y leaf:X = BinTreeAug X Y;
bta_fork$1 {X:Type} {Y:Type} left:^(BinTreeAug X Y)
right:^(BinTreeAug X Y) extra:Y = BinTreeAug X Y;

바이너리 트리 키 생성 메커니즘은 표준화된 해시맵 프레임워크와 유사한 방식으로 작동하지만 레이블을 사용하지 않고 분기 접두사만 포함합니다.

주소

TON 주소는 TL-B StateInit 구조를 사용하는 sha256 해싱 메커니즘으로 형성됩니다. 즉, 네트워크 컨트랙트를 배포하기 전에 주소를 계산할 수 있습니다.

직렬화

EQBL2_3lMiyywU17g-or8N7v9hDmPCpttzBPE2isF2GTzpK4`와 같은 표준 주소는 바이트 인코딩을 위해 base64 uri를 사용합니다. 일반적으로 36바이트의 길이를 가지며, 이 중 마지막 2바이트는 XMODEM 테이블로 계산된 crc16 체크섬이고, 첫 번째 바이트는 플래그를 나타내고, 두 번째 바이트는 워크체인을 나타냅니다. 중간에 있는 32바이트는 주소 자체의 데이터(AccountID라고도 함)로, 보통 int256과 같은 스키마로 표현됩니다.

디코딩 예시

참조

여기 Oleg Baranov원본 기사 링크가 있습니다.