메시지 보내기
메시지 구성, 구문 분석 및 전송은 TL-B 스키마, 트랜잭션 단계 및 TVM의 교차점에 있습니다.
실제로 FunC는 직렬화된 메시지를 인자로 받는 send_raw_message 함수를 노출하고 있습니다.
TON은 다양한 기능을 갖춘 포괄적인 시스템이기 때문에 이 모든 기능을 지원할 수 있어야 하는 메시지는 상당히 복잡해 보일 수 있습니다. 하지만 이러한 기능의 대부분은 일반적인 시나리오에서는 사용되지 않으며, 대부분의 경우 메시지 직렬화는 다음과 같이 축소될 수 있습니다:
cell msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(addr)
.store_coins(amount)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_slice(message_body)
.end_cell();
따라서 개발자는 두려워할 필요가 없으며 이 문서를 처음 읽었을 때 이해하기 어려운 부분이 있어도 괜찮습니다. 일반적인 개념만 파악하면 됩니다.
문서에서 **'그램'**이라는 단어가 언급되는 경우가 있는데, 대부분 코드 예제에서 톤코인의 오래된 이름일 뿐입니다.
자세히 알아봅시다!
메시지 유형
메시지에는 세 가지 유형이 있습니다:
- 외부 메시지는 블록체인 외부에서 블록체인 내부의 스마트 콘트랙트로 전송되는 메시지입니다. 이러한 메시지는 소위 '신용 가스' 동안 스마트 콘트랙트에 의해 명시적으로 수락되어야 합니다. 메시지가 수락되지 않으면 노드는 이를 블록으로 수락하거나 다른 노드에 전달해서는 안 됩니다.
- 한 블록체인 엔티티에서 다른 블록체인 엔티티로 전송되는 내부 메시지입니다. 이러한 메시지는 (외부 메시지와 달리) 약간의 톤을 전달하고 스스로 비용을 지불할 수 있습니다. 따라서 이러한 메시지를 수신하는 스마트 콘트랙트는 이를 수락하지 않을 수 있습니다. 이 경우 메시지 값에서 가스가 차감됩니다.
- 로그는 블록체인 엔티티에서 외부 세계로 전송되는 메시지입니다. 일반적으로 이러한 메시지를 블록체인 외부로 전송하는 메커니즘은 없습니다. 실제로 네트워크의 모든 노드는 메시지의 생성 여부에 대한 합의를 가지고 있지만, 이를 처리하는 방법에 대한 규칙은 없습니다. 로그를
/dev/null
로 직접 전송하거나, 디스크에 기록하거나, 인덱싱된 데이터베이스에 저장하거나, 블록체인 이외의 수단(이메일/텔레그램/SMS)으로 전송할 수도 있지만, 이 모든 것은 해당 노드의 재량에 따라 달라질 수 있습니다.
메시지 레이아웃
내부 메시지 레이아웃부터 시작하겠습니다.
스마트 컨트랙트로 전송할 수 있는 메시지를 설명하는 TL-B 체계는 다음과 같습니다:
message$_ {X:Type} info:CommonMsgInfoRelaxed
init:(Maybe (Either StateInit ^StateInit))
body:(Either X ^X) = MessageRelaxed X;
말로 표현해 봅시다. 모든 메시지의 직렬화는 정보(소스, 대상 및 기타 메타데이터를 설명하는 일종의 헤더), init(메시지 초기화에만 필요한 필드), 본문(메시지 페이로드)의 세 가지 필드로 구성됩니다.
아마도및
둘 중 하나` 및 기타 유형의 표현식은 다음과 같은 의미를 갖습니다:
- 정보:CommonMsgInfoRelaxed
필드가 있는 경우, 이는
CommonMsgInfoRelaxed`의 직렬화가 직렬화 셀에 직접 주입된다는 의미입니다. - body:(Either X ^ X)
필드가 있을 때, 어떤 타입
X를 (탈)직렬화할 때, 먼저
X가 같은 셀에 직렬화되면
0, 별도의 셀에 직렬화되면
1` 비트 하나를 넣는다는 뜻입니다. - 초기화:(어쩌면 (StateInit ^StateInit))
필드가 있을 때, 이 필드가 비어 있는지 여부에 따라 먼저
0또는
1을 넣고, 비어 있지 않으면
Either StateInit ^StateInit을 직렬화한다는 의미입니다(다시 말해,
StateInit이 같은 셀에 직렬화되면
0, 별도의 셀에 직렬화되면
1인
either` 비트 하나를 넣습니다.).
CommonMsgInfoRelaxed` 레이아웃은 다음과 같습니다:
int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
src:MsgAddress dest:MsgAddressInt
value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams
created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed;
ext_out_msg_info$11 src:MsgAddress dest:MsgAddressExt
created_lt:uint64 created_at:uint32 = CommonMsgInfoRelaxed;
지금은 int_msg_info
에 집중해 보겠습니다.
1비트 접두사 0
으로 시작하여 인스턴트 하이퍼큐브 라우팅 비활성화 여부(현재 항상 true), 메시지 처리 중 오류 발생 시 반송 여부, 메시지 자체 반송 여부 등 세 가지 1비트 플래그가 있습니다. 그런 다음 발신지와 수신지 주소가 직렬화된 다음 메시지 값과 메시지 전달 수수료 및 시간과 관련된 네 개의 정수가 이어집니다.
스마트 컨트랙트에서 메시지가 전송되면 일부 필드가 올바른 값으로 다시 작성됩니다. 특히, 검증자는 bounced
, src
, ihr_fee
, fwd_fee
, created_lt
및 created_at
를 다시 작성합니다. 이는 두 가지를 의미합니다: 첫째, 메시지를 처리하는 동안 다른 스마트 컨트랙트가 해당 필드를 신뢰할 수 있고(발신자가 소스 주소, bounced
플래그 등을 위조할 수 없음), 둘째, 직렬화 중에 해당 필드에 유효한 값을 넣을 수 있다는 것입니다(어쨌든 해당 값은 덮어쓰게 됩니다).
메시지를 간단하게 직렬화하면 다음과 같습니다:
var msg = begin_cell()
.store_uint(0, 1) ;; tag
.store_uint(1, 1) ;; ihr_disabled
.store_uint(1, 1) ;; allow bounces
.store_uint(0, 1) ;; not bounced itself
.store_slice(source)
.store_slice(destination)
;; serialize CurrencyCollection (see below)
.store_coins(amount)
.store_dict(extra_currencies)
.store_coins(0) ;; ihr_fee
.store_coins(fwd_value) ;; fwd_fee
.store_uint(cur_lt(), 64) ;; lt of transaction
.store_uint(now(), 32) ;; unixtime of transaction
.store_uint(0, 1) ;; no init-field flag (Maybe)
.store_uint(0, 1) ;; inplace message body flag (Either)
.store_slice(msg_body)
.end_cell();
그러나 일반적으로 개발자는 모든 필드를 단계별로 직렬화하는 대신 바로가기를 사용합니다. 따라서 elector-code의 예시를 통해 스마트 컨트랙트에서 메시지를 전송하는 방법을 고려해 보겠습니다.
() send_message_back(addr, ans_tag, query_id, body, grams, mode) impure inline_ref {
;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddress -> 011000
var msg = begin_cell()
.store_uint(0x18, 6)
.store_slice(addr)
.store_coins(grams)
.store_uint(0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
.store_uint(ans_tag, 32)
.store_uint(query_id, 64);
if (body >= 0) {
msg~store_uint(body, 32);
}
send_raw_message(msg.end_cell(), mode);
}
먼저 0x18
값을 0b011000
에 해당하는 6비트에 넣습니다. 이게 뭐죠?
첫 번째 비트는
0
-1비트 접두사로int_msg_info
임을 나타냅니다.그러면
1
,1
,0
의 3비트로 인스턴트 하이퍼큐브 라우팅이 비활성화되고 메시지가 반송될 수 있으며 해당 메시지 자체가 반송된 것이 아님을 의미합니다.그런 다음 발신자 주소가 있어야 하지만 어쨌든 동일한 효과로 재작성되므로 유효한 주소가 저장될 수 있습니다. 가장 짧은 유효한 주소 직렬화는
addr_none
의 직렬화이며 2비트 문자열00
으로 직렬화됩니다.
따라서 .store_uint(0x18, 6)
는 태그와 처음 4개의 필드를 직렬화하는 최적화된 방식입니다.
다음 줄은 대상 주소를 직렬화합니다.
그런 다음 값을 직렬화해야 합니다. 일반적으로 메시지 값은 다음과 같은 스키마를 가진 CurrencyCollection
객체입니다:
nanograms$_ amount:(VarUInteger 16) = Grams;
extra_currencies$_ dict:(HashmapE 32 (VarUInteger 32))
= ExtraCurrencyCollection;
currencies$_ grams:Grams other:ExtraCurrencyCollection
= CurrencyCollection;
이 방식은 메시지에 톤 값 외에도 추가 외부 통화 사전이 포함될 수 있음을 의미합니다. 그러나 현재로서는 이를 무시하고 메시지 값이 "가변 정수인 나노톤 수"와 "0
- 빈 사전 비트"로 직렬화된다고 가정할 수 있습니다.
실제로 위의 선거인 코드에서는 '.store_coins(toncoins)를 통해 코인의 금액을 직렬화한 다음
1 + 4 + 4 + 64 + 32 + 1 + 1`과 같은 길이의 0 문자열을 넣습니다. 이게 뭔가요?
- 첫 번째 비트는 빈 외화 사전을 의미합니다.
- 그러면 4비트 길이의 필드 두 개가 있습니다. 0을
VarUInteger 16
으로 인코딩합니다. 사실ihr_fee
와fwd_fee
는 덮어쓰게 되므로 0을 넣는 것이 좋습니다. - 그런 다음
created_lt
및created_at
필드에 0을 넣습니다. 이러한 필드도 덮어쓰기되지만 수수료와 달리 이러한 필드는 길이가 고정되어 있으므로 64비트 및 32비트 길이의 문자열로 인코딩됩니다. - (우리는 이미 메시지 헤더를 직렬화하여 그 순간에 init/body로 전달했습니다)_.
- 다음 0 비트는
init
필드가 없음을 의미합니다. - 마지막 0 비트는 msg_body가 제자리에서 직렬화됨을 의미합니다.
- 그 후 메시지 본문(임의의 레이아웃 포함)이 인코딩됩니다.
이렇게 하면 14개의 파라미터를 개별적으로 직렬화하는 대신 4개의 직렬화 프리미티브가 실행됩니다.
전체 계획
메시지 레이아웃의 전체 구성표와 모든 구성 필드의 레이아웃(TON의 모든 객체에 대한 구성표)은 block.tlb에 나와 있습니다.
메시지 크기
셀](/학습/개요/셀)은 최대 1023
비트까지 포함할 수 있습니다. 더 많은 데이터를 저장해야 하는 경우 데이터를 청크로 분할하여 참조 셀에 저장해야 합니다.
예를 들어 메시지 본문 크기가 900비트인 경우 메시지 헤더와 같은 셀에 저장할 수 없습니다.
실제로 메시지 헤더 필드 외에도 셀의 총 크기가 1023비트를 초과하게 되며 직렬화 중에 셀 오버플로
예외가 발생합니다. 이 경우 0
대신 1
이 있어야 하며 메시지 본문은 참조 셀에 저장되어야 합니다.
일부 필드는 크기가 가변적이기 때문에 신중하게 처리해야 합니다.
예를 들어, MsgAddress
는 4개의 생성자로 표현할 수 있습니다: 주소없음,
주소표준,
주소외부,
주소변수의 길이가 2비트(
주소없음의 경우)에서 586비트(가장 큰 형식의
주소변수의 경우)까지입니다. 나노톤의 양은
VarUInteger 16으로 직렬화됩니다. 즉, 정수의 바이트 길이를 나타내는 4비트와 정수 자체에 대한 앞의 바이트가 표시됩니다. 이렇게 하면 0 나노톤은
0b0000(길이가 0인 바이트 문자열을 인코딩하는 4비트와 0바이트)으로 직렬화되고, 100.000.000 톤(또는 100000000000000000 나노톤)은
0b10000000000101100011010001010110000101110110001010000000000000(
0b1000`은 8바이트와 8바이트 자체를 의미)으로 직렬화됩니다.
더 많은 구성 매개변수와 값은 여기에서 확인할 수 있습니다.
메시지 모드
아시다시피, 저희는 메시지 자체를 소비하는 것 외에도 모드를 수락하는 send_raw_message
로 메시지를 보냅니다. 필요에 가장 적합한 모드를 찾으려면 다음 표를 참조하세요:
모드 | 설명 |
---|---|
0 | 일반 메시지 |
64 | 새 메시지에 처음 표시된 값 외에 인바운드 메시지의 나머지 값을 모두 전달합니다. |
128 | 원래 메시지에 표시된 값 대신 현재 스마트 컨트랙트의 남은 잔액을 모두 전달합니다. |
깃발 | 설명 |
---|---|
+1 | 메시지 금액과 별도로 송금 수수료 지불 |
+2 | 조치 단계에서 이 메시지를 처리하는 동안 발생하는 일부 오류는 무시하세요(아래 참고 사항 확인). |
+16 | 작업 실패의 경우 - 트랜잭션을 반송합니다. 2`를 사용하면 효과가 없습니다. |
+32 | 현재 계정의 잔액이 0인 경우 해당 계정을 삭제해야 합니다(모드 128과 함께 자주 사용됨). |
- 톤코인이 충분하지 않습니다:
- 메시지와 함께 전송할 값이 충분하지 않습니다(인바운드 메시지 값이 모두 소비됨).
- 메시지를 처리할 자금이 부족합니다.
- 전달 수수료를 지불할 만큼 메시지에 첨부된 가치가 충분하지 않습니다.
- 메시지와 함께 보낼 추가 통화가 충분하지 않습니다.
- 아웃바운드 외부 메시지 비용을 지불할 자금이 부족합니다.
- 메시지가 너무 큽니다(자세한 내용은 [메시지 크기](메시지#메시지 크기)를 확인하세요).
- 메시지의 머클 깊이가 너무 큽니다.
그러나 다음 시나리오에서는 오류를 무시하지 않습니다:
- 메시지의 형식이 잘못되었습니다.
- 메시지 모드에는 64개 및 128개 모드가 모두 포함됩니다.
- 아웃바운드 메시지에 StateInit에 잘못된 라이브러리가 있습니다.
- 외부 메시지가 일반 메시지가 아니거나 +16 또는 +32 플래그 또는 둘 다를 포함합니다. :::
send_raw_message에 대한 모드를 만들려면 모드와 플래그를 합쳐서 조합하면 됩니다. 예를 들어, 일반 메시지를 보내고 송금 수수료를 별도로 지불하려면 모드
0과 플래그
+1을 사용하여
모드 = 1을 얻습니다. 전체 컨트랙트 잔액을 전송하고 즉시 소멸하려면 모드
128과 플래그
+32를 사용해
모드 = 160`을 얻습니다.