English 简体中文 繁體中文 한국 사람 日本語 Deutsch русский بالعربية TÜRKÇE português คนไทย french

简体中文 繁體中文 English 日本語 Deutsch 한국 사람 بالعربية TÜRKÇE คนไทย Français русский

回答

收藏

8.1 处理外部信息

开源社区 开源社区 6770 人阅读 | 0 人回复 | 2025-03-16

💎 欢迎来到第 8 章。
本章将探讨与 TON 中区块链安全和管理相关的各种主题。本章首先深入研究了简单的钱包实现、重放攻击、拒绝服务攻击和合约建议。之后,重点将转移到消息流上,指导你掌握有效使用消息流的必要知识和技能。TON 中的GAS管理是其中一课涉及的另一个重要方面,它使您能够理解并实施高效GAS管理策略。此外,本章还包括一个深入探讨 TON 存储管理的教程,为您提供有效数据存储的基本见解和技术。
您可能已经知道,TON 智能合约有三个可能的入口:
  • 每个合约都必须有一个 recv_internal 函数。
  • 可选的 recv_external 用于接收外部信息。
  • run_ticktock 在特殊智能合约的 ticktock 交易中调用。普通合约不使用。
    7 W9 l1 O8 ~; p* h
下面我们将讨论 recv_external。这个概念很熟悉:用户通常在链外进行交易,用自己的私钥签名,然后发送给合约。
简单的钱包实施
TON 中的钱包只是一个智能合约。让我们写一个简单的钱包,它只转发用我们的私钥签名的信息。基本实现如下:
  1. ;; the only function that can get messages not from other smart contracts
    - x( f+ r$ d1 l8 d5 q$ @- i
  2. () recv_external(slice in_msg) impure {     
    0 G' b/ E7 A. ?9 `* ]# d$ ?- P
  3.     var signature = in_msg~load_bits(512);  ;; read the signature from the incoming message
    & m$ Y% }# [  M9 d' |
  4.     var cs = in_msg;                        ;; make a copy of the incoming message
    9 }! P# _! M  ?$ P+ `( B
  5.     var ds = get_data().begin_parse();      ;; reading the contract storage
    ! F( ~# }& c, H3 j0 v
  6.     var public_key = ds~load_uint(256);     ;; read the authorized key (contract owner)6 K. o9 _9 X4 h, x
  7.     throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));' P* h, [) ~* k* v2 t
  8.     accept_message();                       ;; we are ready to pay for gas to process this( j. ]" K, k6 k
  9.     var mode = cs~load_uint(8);             ;; read the mode from incoming message3 [% ~5 b; v' ]
  10.     send_raw_message(cs~load_ref(), mode);  ;; send the provided message# ?6 }. d# Q7 `
  11. }
复制代码
事实上,该合约将处理合约所有者签署的信息。但没有什么能阻止恶意方读取有效载荷并重新传输。这就是所谓的 "重放攻击"。
重复一遍。攻击者获取合约所有者签署的信息,并再次发送到这个钱包。它将再次被执行。
重放攻击
钱包所有者现在需要提供一个时间戳,直到信息有效为止。但在这段时间内,信息仍可被重放。
  1. () recv_external(slice in_msg) impure {
    3 ?' B. Y) w3 b
  2.     var signature = in_msg~load_bits(512);# X4 Q2 t8 A* i$ d, j5 g
  3.     var cs = in_msg;
    1 K, F; f" L& \7 G
  4.     var valid_until = cs~load_uint(32);         ;; added2 @& D, l  ]& [7 [$ Z
  5.     throw_if(35, valid_until <= now());         ;; added
    5 I2 U6 h/ T' o; s8 N9 ~
  6.     var ds = get_data().begin_parse();
    5 o- a9 ?1 v, f0 f
  7.     var public_key = ds~load_uint(256);
    ! i& z. F, d/ d  x! ]3 u6 f* W# `& |
  8.     throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
    8 Z2 P4 E8 H; S0 }# G
  9.     accept_message();6 f% Q' f9 c; @
  10.     var mode = cs~load_uint(8);
    9 L3 m( o. e4 K7 v
  11.     send_raw_message(cs~load_ref(), mode);
    6 ^& Y0 `% |+ q2 u# ?1 H) U* ~
  12. }
复制代码
重放攻击
钱包所有者现在需要提供一个序列号,该序列号会在存储的序列号上递增。这确保了任何信息都只能被处理一次:
  1. () recv_external(slice in_msg) impure {+ C: I, G6 m4 X1 k5 M1 P7 L- A$ M" E
  2.     var signature = in_msg~load_bits(512);6 k# T3 y  J) T7 o9 Z
  3.     var cs = in_msg;
    * E+ m! R& ^* N( U
  4.     var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32));            ;; changed  w1 ~8 a# ^" I% }9 {
  5.     throw_if(35, valid_until <= now());
    / d, t5 T3 V) m  s3 o
  6.     var ds = get_data().begin_parse();
    ' ^. S* W: `0 C
  7.     var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256));         ;; changed4 i+ N9 v/ {8 z
  8.     throw_unless(33, msg_seqno == stored_seqno);                                    ;; added
    $ U3 ^' j/ ?5 U. Z) e; V2 ~
  9.     throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
    % E5 U" F  n3 @; n" T& i
  10.     accept_message();
    4 Q. G/ ^) ^" ?, k9 P/ P
  11.     var mode = cs~load_uint(8);: g& j& @3 C' I% N2 M
  12.     send_raw_message(cs~load_ref(), mode);
    . {+ n, N8 Z' y6 q
  13. 7 p. i& Q: Q- c0 e6 p( R/ ~$ t  G
  14.     set_data(begin_cell()                                   ;; added7 L, T0 u2 m: X# q% u
  15.         .store_uint(stored_seqno + 1, 32)
    + d/ E  p: K( U7 K
  16.         .store_uint(public_key, 256)
    - s! F; V; l' v$ B+ C
  17.         .end_cell());                                     y% B" y2 n: O) a4 J
  18. }
复制代码
拒绝服务攻击
应谨慎使用 recv_external,并在验证后才使用 accept_message()。否则,GAS可能会因GAS恶意执行而耗尽合约资金
  1. () recv_external(slice in_msg) impure {
    + E/ P" |4 J1 U+ D! J
  2.     var signature = in_msg~load_bits(512);
    & U* G3 h1 m5 m% j! I0 x: u
  3.     var cs = in_msg; 7 h% Q3 I, R0 |+ J
  4.     var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32));    2 C: K. t0 ^4 h0 u! [( y3 _* n
  5.     throw_if(35, valid_until <= now());# k4 |/ y) U# b" a4 F. M
  6.     var ds = get_data().begin_parse(); , b! x/ z0 \, @1 R$ l' V
  7.     var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256));
    6 V2 @8 Q4 _; H. p
  8.     throw_unless(33, msg_seqno == stored_seqno);                                    ; L) Z- Q. L4 ~: a5 W# y6 q! b
  9.     throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
    , L/ `0 e( _$ @$ V

  10. ( J9 v, P$ K0 l; Y: j
  11.     accept_message();                           ;; accept after checking signature only
      `0 N6 }$ c4 C9 S
  12. 6 B& B* ~2 p) e0 @  `7 g8 R
  13.     var mode = cs~load_uint(8);% m* C, L+ N; V8 F( o- K
  14.     send_raw_message(cs~load_ref(), mode);0 ~* |) h4 T2 @' E
  15.     set_data(begin_cell()
    : w7 g0 Q+ \( {( Q9 V7 U
  16.         .store_uint(stored_seqno + 1, 32)8 ^- C# v+ i, Y- j
  17.         .store_uint(public_key, 256)" g! M0 R- _" n
  18.         .end_cell());                                   
    # G3 J6 l& B, k  `% |+ g8 z
  19. }
复制代码
另外请注意,同一签名的信息可以在同一所有者的另一个钱包上重放。或者,测试网络中的信息可以在主网络中重放。
0 c1 j5 q" k' D) k7 m% w" X
失败的重放攻击
请注意,如果在 accept_message() 后出现错误,交易将被写入区块链,费用将从合约余额中扣除,但不会更新存储,也不会像任何有错误退出代码的交易那样执行操作。因此,如果合约接受了一条外部消息,然后由于消息数据出错或发送了一条序列化错误的消息而抛出异常,它将支付处理费用,但却无法防止消息重放。合约会一次又一次地接受相同的消息,直到耗尽全部余额。
这就是为什么我们要在信息解析和执行前保存更新状态的原因:
  1. () recv_external(slice in_msg) impure {% b3 r8 w7 V  T5 f/ }
  2.     var signature = in_msg~load_bits(512);6 ]% G% [( E# r* k
  3.     var cs = in_msg; 2 T, y4 v! o/ D* ?3 U& _
  4.     var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32));   
    * }# F7 L# O7 Z1 Z* H- v
  5.     throw_if(35, valid_until <= now());* d; F8 B& @# h2 _9 V+ B
  6.     var ds = get_data().begin_parse(); + n* h2 |1 H9 J2 Z0 R' O- o
  7.     var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256)); 6 \9 K/ O. e% ]  f7 w4 q* \9 f
  8.     throw_unless(33, msg_seqno == stored_seqno);                                    
    1 [! i+ r9 A" Z: w: V
  9.     throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
    , U3 M! `% ^3 _( P7 i# A" v( t
  10.     accept_message();                           
    # F! P! p5 }: `" ~3 t. u" P; s

  11. ; c: f& h) A  U! \" |$ y
  12.     set_data(begin_cell()                       ;; moved
    ' ^: A3 g/ t- R
  13.         .store_uint(stored_seqno + 1, 32)
    . ]9 w* y  O, P8 _3 [$ s. J
  14.         .store_uint(public_key, 256): K3 x4 R9 T# E" _# |, D7 V5 |
  15.         .end_cell());8 F% g# _% A1 \4 R6 I
  16.     commit();                                   ;; added
    & j0 S) K" C$ w, o: ]

  17. 4 q3 E5 o2 S6 ~( A. a9 Q
  18.     var mode = cs~load_uint(8);                 ;; can fail
    $ p. e' A8 C# |( T
  19.     send_raw_message(cs~load_ref(), mode);      ;; can fail4 _! H( l% k: k+ G# K9 W* Z0 q+ Y
  20. }
复制代码
EVM vs TON
在以太坊虚拟机(EVM)中,重放保护并不在讨论范畴之内的:
  • 私钥只有一个相关地址
  • 每次处理交易后,nonce  会自动增加
  • 钱包所有者支付 GAS,失败的交易无法重放
    # Z& d" y: N" J3 ?$ N, i
TON 更为灵活:
建议
幸运的是,对于大多数合约开发人员来说,有一个简单的建议:避免处理外部消息。
  • 让 "钱包 "合约来完成这项工作,在你的合约中只包含 recv_internal。
  • 即使您编写的是 "多重签名 "或特殊钱包,您仍然可以只处理来自普通钱包的内部信息。
  • 在这种情况下,您将获得 EVM 风格的交易,并拥有底层的所有灵活性。

    + S9 T& |' E3 }0 ^

3 M/ T" C/ y% A/ T( o) i
- K# \5 ^4 l. a# z" V0 @) V8 \
分享到:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则