💎 欢迎来到第 8 章。 本章将探讨与 TON 中区块链安全和管理相关的各种主题。本章首先深入研究了简单的钱包实现、重放攻击、拒绝服务攻击和合约建议。之后,重点将转移到消息流上,指导你掌握有效使用消息流的必要知识和技能。TON 中的GAS管理是其中一课涉及的另一个重要方面,它使您能够理解并实施高效GAS管理策略。此外,本章还包括一个深入探讨 TON 存储管理的教程,为您提供有效数据存储的基本见解和技术。 您可能已经知道,TON 智能合约有三个可能的入口: 每个合约都必须有一个 recv_internal 函数。 可选的 recv_external 用于接收外部信息。 run_ticktock 在特殊智能合约的 ticktock 交易中调用。普通合约不使用。 / H; q4 E2 f1 P% `
下面我们将讨论 recv_external。这个概念很熟悉:用户通常在链外进行交易,用自己的私钥签名,然后发送给合约。 简单的钱包实施TON 中的钱包只是一个智能合约。让我们写一个简单的钱包,它只转发用我们的私钥签名的信息。基本实现如下: - ;; the only function that can get messages not from other smart contracts
. w% `8 s7 G3 g% p - () recv_external(slice in_msg) impure {
9 V( r4 k! i3 T( k3 | - var signature = in_msg~load_bits(512); ;; read the signature from the incoming message1 [! x% a5 h6 F4 z6 d$ c2 E
- var cs = in_msg; ;; make a copy of the incoming message1 u" T( H$ v: H. f
- var ds = get_data().begin_parse(); ;; reading the contract storage2 a W* o; @, S" n: V5 d
- var public_key = ds~load_uint(256); ;; read the authorized key (contract owner)
: |7 i9 U0 f4 Y, I0 j6 Z2 g - throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));6 L# r( ]3 X& U \2 Y
- accept_message(); ;; we are ready to pay for gas to process this' }/ H& S8 A2 N6 J* B8 E. t2 W n7 H1 C
- var mode = cs~load_uint(8); ;; read the mode from incoming message; A: g4 l6 ^) R$ A/ p4 ?
- send_raw_message(cs~load_ref(), mode); ;; send the provided message
( r$ n0 ^, X8 v2 ?3 d - }
复制代码事实上,该合约将处理合约所有者签署的信息。但没有什么能阻止恶意方读取有效载荷并重新传输。这就是所谓的 "重放攻击"。 重复一遍。攻击者获取合约所有者签署的信息,并再次发送到这个钱包。它将再次被执行。 重放攻击钱包所有者现在需要提供一个时间戳,直到信息有效为止。但在这段时间内,信息仍可被重放。 - () recv_external(slice in_msg) impure {' k& ]9 F# B1 D5 ^4 I3 B* f& m D
- var signature = in_msg~load_bits(512);( W! o A) l# r# u+ @; ] f- G
- var cs = in_msg;
! D' }: d3 J( W* Y3 S - var valid_until = cs~load_uint(32); ;; added# w& |& s- w K ?
- throw_if(35, valid_until <= now()); ;; added
) z& E' r3 n8 v& K: j2 p - var ds = get_data().begin_parse();
$ R' Y% P8 H/ P* ` X- a5 r/ D; @% } - var public_key = ds~load_uint(256);
& c: e" m2 ]8 @ L" I5 a - throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
5 u" _9 _. i4 o+ o* D$ H3 Z0 l- ^- N! u- M - accept_message();
: k+ S% q7 k: K4 C7 G& s; h* F4 V. m - var mode = cs~load_uint(8);9 B5 ~; J3 a! c B9 R
- send_raw_message(cs~load_ref(), mode);1 \8 `# ^, V5 E" S/ j
- }
复制代码 重放攻击钱包所有者现在需要提供一个序列号,该序列号会在存储的序列号上递增。这确保了任何信息都只能被处理一次: - () recv_external(slice in_msg) impure {
/ D0 V* h/ H6 A- e; J }0 J, \ - var signature = in_msg~load_bits(512);+ K! s1 L3 L7 r. {* p0 L& Y2 \3 l4 Y4 |
- var cs = in_msg;
* [7 z6 S3 E" O }: | - var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32)); ;; changed0 {2 U% X& L0 P5 M. b) s( L( C
- throw_if(35, valid_until <= now());- A& r0 l- a4 i" G- u' [
- var ds = get_data().begin_parse(); & E0 O& u/ ^; B
- var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256)); ;; changed5 W7 O# O* w; ]7 B
- throw_unless(33, msg_seqno == stored_seqno); ;; added
+ c8 y/ h6 ~* | _3 x - throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));* w/ S2 c) R6 B) C3 d& p2 b8 U
- accept_message();/ K2 n4 R8 |; g- Z
- var mode = cs~load_uint(8);, e- i) O' i8 z; t- b0 g
- send_raw_message(cs~load_ref(), mode);
2 I0 x' r$ [. y% P/ R) W- v - 7 p9 r7 C# N5 ]3 ?7 O; m, V5 S0 f1 {6 f
- set_data(begin_cell() ;; added2 p9 N5 t9 q( s5 u, ?+ w# h' g
- .store_uint(stored_seqno + 1, 32)
1 y* T4 w* G3 F/ @, Q( z# r - .store_uint(public_key, 256)
* T* l% t7 U9 q. P( R; v - .end_cell()); : I. C2 e, `$ H' Q; ]1 w
- }
复制代码 拒绝服务攻击应谨慎使用 recv_external,并在验证后才使用 accept_message()。否则,GAS可能会因GAS恶意执行而耗尽合约资金 - () recv_external(slice in_msg) impure {2 t0 @1 P. \" Q: x- n2 h
- var signature = in_msg~load_bits(512);
# L+ A! E- f' _' k9 W" Z+ L - var cs = in_msg;
: y# m% `/ @6 I - var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32)); 0 f. S0 }0 I7 J3 U% I
- throw_if(35, valid_until <= now());3 ]) S _1 }( X" T+ q
- var ds = get_data().begin_parse();
- v3 s1 {6 ] D' c - var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256));
8 a6 m a4 t* c+ M/ T+ k; w4 e% J - throw_unless(33, msg_seqno == stored_seqno); & D/ X8 s6 I b, U
- throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));$ Q/ l& `) u% w# d
- ) Q+ G$ z o; L$ o( n& @
- accept_message(); ;; accept after checking signature only, q* { ~2 H; i3 @: L+ P
- 7 ?3 I0 O/ ?) T; h% H8 T; H0 q" Q
- var mode = cs~load_uint(8);
( }) d; E+ F" e2 W# d$ J - send_raw_message(cs~load_ref(), mode);2 @0 p( I+ m/ B
- set_data(begin_cell()
' O3 [8 Z- x$ F. u0 a0 i* W0 N - .store_uint(stored_seqno + 1, 32)' n# V/ W9 y0 F: `- b
- .store_uint(public_key, 256)) q1 I) d) M" A: ~4 U% p
- .end_cell()); 4 q h# D7 ^; ]5 l \( X& R! X! ~
- }
复制代码另外请注意,同一签名的信息可以在同一所有者的另一个钱包上重放。或者,测试网络中的信息可以在主网络中重放。 * V* W- I" D( c
失败的重放攻击请注意,如果在 accept_message() 后出现错误,交易将被写入区块链,费用将从合约余额中扣除,但不会更新存储,也不会像任何有错误退出代码的交易那样执行操作。因此,如果合约接受了一条外部消息,然后由于消息数据出错或发送了一条序列化错误的消息而抛出异常,它将支付处理费用,但却无法防止消息重放。合约会一次又一次地接受相同的消息,直到耗尽全部余额。 这就是为什么我们要在信息解析和执行前保存更新状态的原因: - () recv_external(slice in_msg) impure { L; o- z. ~% s* D* I
- var signature = in_msg~load_bits(512);" O! \# t3 N" l! t# B' {
- var cs = in_msg; $ c: R* e0 k. _" S* U
- var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32));
, Y* Y1 T" n3 f V- f - throw_if(35, valid_until <= now());
. t: c2 m/ V& U# f: V. P" B - var ds = get_data().begin_parse(); 8 H. f# M8 o3 u2 _; r, I6 P3 d1 G
- var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256)); 2 H2 s& o3 [8 W, p- ]
- throw_unless(33, msg_seqno == stored_seqno); - T/ q2 C8 i( k! j
- throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
. L# ^1 y9 F( W, O% T6 n - accept_message();
! e5 m$ e, X8 _9 g# E - . h1 ]5 h- M/ @! W0 r
- set_data(begin_cell() ;; moved. ~& a5 W7 S, Q; V
- .store_uint(stored_seqno + 1, 32)
5 \# R$ H2 H5 C; L8 x) b$ |5 @# E: | k - .store_uint(public_key, 256): X3 N" y( N' e+ x. E1 D6 v3 k
- .end_cell());% N* d. g7 t% A1 T
- commit(); ;; added
. c% ^0 c1 J" h- _( ]- g2 p
1 I/ Z4 m) C- _, z/ Z! S ] z- var mode = cs~load_uint(8); ;; can fail ?) V( K# x A
- send_raw_message(cs~load_ref(), mode); ;; can fail# K* W$ ]' V& z6 L/ ^
- }
复制代码 EVM vs TON在以太坊虚拟机(EVM)中,重放保护并不在讨论范畴之内的: 私钥只有一个相关地址 每次处理交易后,nonce 会自动增加 钱包所有者支付 GAS,失败的交易无法重放
; q1 a9 f* r* e# p- J, N, b
TON 更为灵活: 一个私人密钥可控制多个钱包 钱包可编程 - . j1 ]; V/ X6 c5 @, G
建议幸运的是,对于大多数合约开发人员来说,有一个简单的建议:避免处理外部消息。 让 "钱包 "合约来完成这项工作,在你的合约中只包含 recv_internal。 即使您编写的是 "多重签名 "或特殊钱包,您仍然可以只处理来自普通钱包的内部信息。 在这种情况下,您将获得 EVM 风格的交易,并拥有底层的所有灵活性。
6 M) Q5 _# P+ B$ n# I& N
" d4 D* H: B- z, N# C( w7 h8 Y) p a r& s7 W t2 S
|