💎 欢迎来到第 8 章。 本章将探讨与 TON 中区块链安全和管理相关的各种主题。本章首先深入研究了简单的钱包实现、重放攻击、拒绝服务攻击和合约建议。之后,重点将转移到消息流上,指导你掌握有效使用消息流的必要知识和技能。TON 中的GAS管理是其中一课涉及的另一个重要方面,它使您能够理解并实施高效GAS管理策略。此外,本章还包括一个深入探讨 TON 存储管理的教程,为您提供有效数据存储的基本见解和技术。 您可能已经知道,TON 智能合约有三个可能的入口: 每个合约都必须有一个 recv_internal 函数。 可选的 recv_external 用于接收外部信息。 run_ticktock 在特殊智能合约的 ticktock 交易中调用。普通合约不使用。 ' g( }3 R1 }7 ~/ M; J: i
下面我们将讨论 recv_external。这个概念很熟悉:用户通常在链外进行交易,用自己的私钥签名,然后发送给合约。 简单的钱包实施TON 中的钱包只是一个智能合约。让我们写一个简单的钱包,它只转发用我们的私钥签名的信息。基本实现如下: - ;; the only function that can get messages not from other smart contracts
* {7 Z8 i% }- r6 x - () recv_external(slice in_msg) impure {
" t! i+ g8 K) J, t$ X7 O; v - var signature = in_msg~load_bits(512); ;; read the signature from the incoming message5 y+ g0 U" n3 s% T' j
- var cs = in_msg; ;; make a copy of the incoming message
: G5 C( t' G( G3 x# V - var ds = get_data().begin_parse(); ;; reading the contract storage& T- c, o% F5 T, u+ x
- var public_key = ds~load_uint(256); ;; read the authorized key (contract owner)% e. _ ~ m! Q# H8 ]
- throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
' B: F% c2 X/ {, p* d - accept_message(); ;; we are ready to pay for gas to process this7 k! f* M% ?! C6 Z
- var mode = cs~load_uint(8); ;; read the mode from incoming message
7 i* L3 i% E k( _) w7 v - send_raw_message(cs~load_ref(), mode); ;; send the provided message
7 @% y0 h5 }) ^' A# o! } - }
复制代码事实上,该合约将处理合约所有者签署的信息。但没有什么能阻止恶意方读取有效载荷并重新传输。这就是所谓的 "重放攻击"。 重复一遍。攻击者获取合约所有者签署的信息,并再次发送到这个钱包。它将再次被执行。 重放攻击钱包所有者现在需要提供一个时间戳,直到信息有效为止。但在这段时间内,信息仍可被重放。 - () recv_external(slice in_msg) impure {
9 g; [. r L0 c: p - var signature = in_msg~load_bits(512);
+ t' G* P6 G/ q5 b- ^' {$ d1 l - var cs = in_msg; 4 K# ]+ A6 x# a! v/ j+ c
- var valid_until = cs~load_uint(32); ;; added
' i+ V) _; a( D9 ]8 }3 o3 k6 _ - throw_if(35, valid_until <= now()); ;; added$ W" I3 h. E) w1 J& B" f1 m: B
- var ds = get_data().begin_parse();
+ m) {* ^9 o( S - var public_key = ds~load_uint(256);
. m/ _: c4 `2 I! ^: ~+ B0 }4 g - throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
1 A0 E+ I9 t0 ]# N9 X8 G# W5 d7 @ - accept_message();
% m4 E6 e) k* E - var mode = cs~load_uint(8);
# o! z3 _2 S/ A) ^ - send_raw_message(cs~load_ref(), mode);
& I+ i) P, O( _ - }
复制代码 重放攻击钱包所有者现在需要提供一个序列号,该序列号会在存储的序列号上递增。这确保了任何信息都只能被处理一次: - () recv_external(slice in_msg) impure {
$ l# g' [& b, r - var signature = in_msg~load_bits(512);0 b# w, u8 ^5 s
- var cs = in_msg; ( h; z- g( F2 K0 M- i' W
- var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32)); ;; changed* _1 f* D% ?! o6 F% w+ U) r
- throw_if(35, valid_until <= now());
; q9 s$ q7 |8 L- R6 n7 } - var ds = get_data().begin_parse(); ' [: E! `7 C4 O
- var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256)); ;; changed
6 C3 Y/ H/ H9 E/ S3 Z - throw_unless(33, msg_seqno == stored_seqno); ;; added
+ x4 _! U7 P5 k U. @9 Y0 M - throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
# A( R+ ?' v8 f" D2 P. n - accept_message();
0 Y5 }9 c& W/ j; T9 M2 B- } - var mode = cs~load_uint(8);
7 _; t2 ], B% z1 v) h3 N1 ?3 k+ c - send_raw_message(cs~load_ref(), mode);% U0 p/ y" ~! C; ]+ H- |3 |! g
: a, l+ J4 ?. k# W- set_data(begin_cell() ;; added
; b P2 U. @8 h, ~' B - .store_uint(stored_seqno + 1, 32)( P2 b0 Z& f9 V4 F$ b% B) D
- .store_uint(public_key, 256)
6 p, o& }1 ~3 B1 w# T1 Q0 T4 a - .end_cell());
T' i+ h# z4 D# R! A - }
复制代码 拒绝服务攻击应谨慎使用 recv_external,并在验证后才使用 accept_message()。否则,GAS可能会因GAS恶意执行而耗尽合约资金 - () recv_external(slice in_msg) impure {
6 T2 Q+ z+ {& a3 O- }9 n9 g0 u - var signature = in_msg~load_bits(512);; D- [6 s' Z7 f$ ]# F3 ?' ]
- var cs = in_msg;
! J& `+ o* X* d - var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32));
& P8 U |7 U* k0 t9 d - throw_if(35, valid_until <= now());
# C$ `6 x% @- T& p1 m - var ds = get_data().begin_parse();
1 x8 L5 k' H1 K2 y! \0 | - var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256)); 5 s( l, J( }" C! T' E
- throw_unless(33, msg_seqno == stored_seqno);
4 ?3 p" P+ g& r/ Q2 J) t3 G) f - throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
2 o; i- a1 [3 O. a% |% g8 ~
' O7 l$ V: |* ?: \# t$ C" ?, i- accept_message(); ;; accept after checking signature only* }# s: [9 ^/ Q$ S
7 h) P7 ^. q" I9 @- var mode = cs~load_uint(8);( D0 E, m0 [( w8 Q1 v# ]
- send_raw_message(cs~load_ref(), mode);
- v8 s4 B3 g! w1 T2 `! {% x - set_data(begin_cell()
: X. N% P' I) ~! S% P1 u - .store_uint(stored_seqno + 1, 32)
4 W+ }, ]6 Y3 O% B* O% q( u - .store_uint(public_key, 256)' c0 Z7 G: s" D& B; f/ r
- .end_cell());
: S% ?/ l4 K' t! w1 l- @" U! G# g - }
复制代码另外请注意,同一签名的信息可以在同一所有者的另一个钱包上重放。或者,测试网络中的信息可以在主网络中重放。 7 t4 J) T9 e' k# L' D8 P
失败的重放攻击请注意,如果在 accept_message() 后出现错误,交易将被写入区块链,费用将从合约余额中扣除,但不会更新存储,也不会像任何有错误退出代码的交易那样执行操作。因此,如果合约接受了一条外部消息,然后由于消息数据出错或发送了一条序列化错误的消息而抛出异常,它将支付处理费用,但却无法防止消息重放。合约会一次又一次地接受相同的消息,直到耗尽全部余额。 这就是为什么我们要在信息解析和执行前保存更新状态的原因: - () recv_external(slice in_msg) impure {! B& j$ V% E) d
- var signature = in_msg~load_bits(512);
7 B3 O2 Z4 U& P, |: { k# l- C - var cs = in_msg; / H9 y) H) l2 d+ V, T' P4 m, p1 x% C9 |
- var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32)); # C$ `% r4 m) \, P0 h9 F" I
- throw_if(35, valid_until <= now());5 W$ T/ q, x+ \& V9 u Z
- var ds = get_data().begin_parse();
% V- D D* F4 Y; \8 }+ T - var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256));
" }! b$ g. j H. h0 d8 T7 l. z9 Z - throw_unless(33, msg_seqno == stored_seqno); % _# |2 U2 y- U3 ]$ R
- throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));8 X: X v% I# j- ^+ c& I
- accept_message();
. G% x! F% A- k6 ~ - 6 d& b0 p* q' c1 h) @# A6 j4 |
- set_data(begin_cell() ;; moved
2 I9 u0 x* W! N2 W! C3 z4 [ - .store_uint(stored_seqno + 1, 32)7 D- ~/ S' w1 T, S8 _
- .store_uint(public_key, 256)( _* u7 @/ z5 X) l7 D2 f" h
- .end_cell());
W P, l0 N* h% s( x5 B5 U - commit(); ;; added
( O1 B* Q/ V) U( ^9 c3 a# \ - # ]* A) U% p% x: T
- var mode = cs~load_uint(8); ;; can fail9 }7 ^2 X' [& G
- send_raw_message(cs~load_ref(), mode); ;; can fail# h4 C! K3 a' s
- }
复制代码 EVM vs TON在以太坊虚拟机(EVM)中,重放保护并不在讨论范畴之内的: 私钥只有一个相关地址 每次处理交易后,nonce 会自动增加 钱包所有者支付 GAS,失败的交易无法重放
7 r8 ?! \. u* q" C/ J; V
TON 更为灵活: 一个私人密钥可控制多个钱包 钱包可编程
0 {9 f* j5 ~ ~( t( a: N8 h3 q0 L 建议幸运的是,对于大多数合约开发人员来说,有一个简单的建议:避免处理外部消息。 让 "钱包 "合约来完成这项工作,在你的合约中只包含 recv_internal。 即使您编写的是 "多重签名 "或特殊钱包,您仍然可以只处理来自普通钱包的内部信息。 在这种情况下,您将获得 EVM 风格的交易,并拥有底层的所有灵活性。
9 t8 ]" r4 |% b0 j5 z
! ]6 f; W' e1 a$ o5 F- e$ ~5 h/ w3 J0 T3 }; u$ W9 s
|