💎 欢迎来到第 8 章。 本章将探讨与 TON 中区块链安全和管理相关的各种主题。本章首先深入研究了简单的钱包实现、重放攻击、拒绝服务攻击和合约建议。之后,重点将转移到消息流上,指导你掌握有效使用消息流的必要知识和技能。TON 中的GAS管理是其中一课涉及的另一个重要方面,它使您能够理解并实施高效GAS管理策略。此外,本章还包括一个深入探讨 TON 存储管理的教程,为您提供有效数据存储的基本见解和技术。 您可能已经知道,TON 智能合约有三个可能的入口: 每个合约都必须有一个 recv_internal 函数。 可选的 recv_external 用于接收外部信息。 run_ticktock 在特殊智能合约的 ticktock 交易中调用。普通合约不使用。 8 u3 e; a/ W4 P
下面我们将讨论 recv_external。这个概念很熟悉:用户通常在链外进行交易,用自己的私钥签名,然后发送给合约。 简单的钱包实施TON 中的钱包只是一个智能合约。让我们写一个简单的钱包,它只转发用我们的私钥签名的信息。基本实现如下: - ;; the only function that can get messages not from other smart contracts# C" U6 B" B0 L. _
- () recv_external(slice in_msg) impure { 7 }# U% a4 }; [; j# @; W
- var signature = in_msg~load_bits(512); ;; read the signature from the incoming message% @2 v6 @& K0 M
- var cs = in_msg; ;; make a copy of the incoming message6 |6 w4 G8 m& I8 Y% M' L
- var ds = get_data().begin_parse(); ;; reading the contract storage
- T( |% H7 b; O, M) Q+ T - var public_key = ds~load_uint(256); ;; read the authorized key (contract owner)
4 u* I2 j. v2 ?/ c8 I2 p - throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
6 p! G) c- w$ K - accept_message(); ;; we are ready to pay for gas to process this- w( `& Q( R1 n& A
- var mode = cs~load_uint(8); ;; read the mode from incoming message
4 w2 G3 s/ ]$ g. @7 \2 h7 R - send_raw_message(cs~load_ref(), mode); ;; send the provided message' `: a% x$ b W- p1 i6 |' c( x6 {
- }
复制代码事实上,该合约将处理合约所有者签署的信息。但没有什么能阻止恶意方读取有效载荷并重新传输。这就是所谓的 "重放攻击"。 重复一遍。攻击者获取合约所有者签署的信息,并再次发送到这个钱包。它将再次被执行。 重放攻击钱包所有者现在需要提供一个时间戳,直到信息有效为止。但在这段时间内,信息仍可被重放。 - () recv_external(slice in_msg) impure {
* ~* g" p9 u' L3 Q/ @# O Y - var signature = in_msg~load_bits(512);
: @$ f/ R: y) l6 U$ T - var cs = in_msg;
' A4 f9 X+ B3 _: T& B - var valid_until = cs~load_uint(32); ;; added
1 l7 p8 g5 M( H1 Q - throw_if(35, valid_until <= now()); ;; added" A: n: i) h: y0 ?5 u9 h
- var ds = get_data().begin_parse(); u( g8 S) O7 w6 z, N4 p
- var public_key = ds~load_uint(256);
% S9 x" f, [$ k, }/ V- G - throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
0 P3 f' J0 p, ^) D* s: i3 ], l- G - accept_message();
, V* U+ q/ P, f+ n - var mode = cs~load_uint(8);
3 X" ~& k! }9 ?6 o% \4 c3 P - send_raw_message(cs~load_ref(), mode);, t' X- \0 z- Q5 P% s
- }
复制代码 重放攻击钱包所有者现在需要提供一个序列号,该序列号会在存储的序列号上递增。这确保了任何信息都只能被处理一次: - () recv_external(slice in_msg) impure {
! d2 J: Q# x% N# d# @ - var signature = in_msg~load_bits(512);1 I5 \/ r$ @+ Q
- var cs = in_msg; % @& f3 y8 S/ D& e
- var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32)); ;; changed
' A8 X1 p3 ~7 _! U7 f0 }* S) ^$ ] - throw_if(35, valid_until <= now());
7 s$ `+ X0 V5 ?. J. r! ]# i7 q. ] - var ds = get_data().begin_parse();
9 B Y4 y" K0 n. s# R$ ]& h - var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256)); ;; changed; t8 L+ H( Z. I& ^
- throw_unless(33, msg_seqno == stored_seqno); ;; added
* \# B1 I" K( M7 ?/ e: @ - throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));- c7 i# y5 d. F7 O7 Z8 n+ u
- accept_message();6 N9 z: \8 i5 n; @9 L
- var mode = cs~load_uint(8);, v' R" F {" S0 O9 |$ Z
- send_raw_message(cs~load_ref(), mode);
8 r( L2 r2 S; Z/ @
; `! N+ O7 H3 {5 l4 A$ B- set_data(begin_cell() ;; added" S; u# V! I' _& o- P( j- a* I
- .store_uint(stored_seqno + 1, 32)
3 U! S7 d; g. m. S3 g9 V - .store_uint(public_key, 256)1 J: |' |0 _, T$ D B
- .end_cell()); # a3 b1 |( J d2 B6 z' }/ R
- }
复制代码 拒绝服务攻击应谨慎使用 recv_external,并在验证后才使用 accept_message()。否则,GAS可能会因GAS恶意执行而耗尽合约资金 - () recv_external(slice in_msg) impure {; w+ l8 W8 K4 O( M- W0 U+ O+ Q6 h
- var signature = in_msg~load_bits(512);
% l; e1 ?1 S0 ^: V - var cs = in_msg;
. R- x4 V4 _5 f - var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32)); 6 c. f @% p0 G8 l$ U8 z' e
- throw_if(35, valid_until <= now());
. m X E3 D! I$ e" D u/ `0 t - var ds = get_data().begin_parse();
. E/ S' ^: c# g$ e - var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256));
% l$ ]6 Z+ }$ T% F - throw_unless(33, msg_seqno == stored_seqno); 0 O/ t; l3 f, R/ o+ E7 a. m% `
- throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));! z6 w* G" l7 a `2 H6 l+ }
( H6 n0 p* P2 J! j1 T- accept_message(); ;; accept after checking signature only% U; L: h& N t; F- C
- % ?+ T0 |* H$ |/ e, j7 C
- var mode = cs~load_uint(8);
8 e( d5 N# C' A - send_raw_message(cs~load_ref(), mode);
2 z9 b, I9 B; \ - set_data(begin_cell()
9 k0 `% R$ ~' o. D' Z - .store_uint(stored_seqno + 1, 32)( `) C2 ~ v8 E$ V
- .store_uint(public_key, 256)
* A$ j: X! l8 u5 x5 @/ F1 r B) [, Z - .end_cell()); ; ^- J4 K/ p) h4 v1 K7 k8 b- H
- }
复制代码另外请注意,同一签名的信息可以在同一所有者的另一个钱包上重放。或者,测试网络中的信息可以在主网络中重放。
0 t- w3 \& u- j3 Q失败的重放攻击请注意,如果在 accept_message() 后出现错误,交易将被写入区块链,费用将从合约余额中扣除,但不会更新存储,也不会像任何有错误退出代码的交易那样执行操作。因此,如果合约接受了一条外部消息,然后由于消息数据出错或发送了一条序列化错误的消息而抛出异常,它将支付处理费用,但却无法防止消息重放。合约会一次又一次地接受相同的消息,直到耗尽全部余额。 这就是为什么我们要在信息解析和执行前保存更新状态的原因: - () recv_external(slice in_msg) impure {
' z( s2 I/ k: d! X - var signature = in_msg~load_bits(512);: ? ~ p* e' B e- v* B
- var cs = in_msg; ) h# O4 T5 a0 U& W+ ^8 d
- var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32)); ( @8 b: }3 A: \0 U
- throw_if(35, valid_until <= now()); n+ E5 V G& X
- var ds = get_data().begin_parse();
9 u- i, G% h6 F! b - var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256)); 3 S* W4 J2 X. h" T
- throw_unless(33, msg_seqno == stored_seqno);
5 C; i) f& I" h: k - throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));! j* T6 j1 I9 j v4 _ ^
- accept_message();
: L A+ m/ P/ n$ j( Y - " a8 k, x4 \: x' ?% I8 x
- set_data(begin_cell() ;; moved7 {6 e. o/ l1 I( [ `+ ~' g* _
- .store_uint(stored_seqno + 1, 32)6 M1 r# V% F; A7 c9 G
- .store_uint(public_key, 256)
. m. r$ }- `4 Q' I: `2 {8 T" |# T - .end_cell());
; S" `: @0 z: _7 ~" G5 @ - commit(); ;; added; h9 H& a/ K& G
& ]" o: @0 Y9 m9 I& o5 m- var mode = cs~load_uint(8); ;; can fail7 D9 \8 J, o3 r) `4 A8 n
- send_raw_message(cs~load_ref(), mode); ;; can fail
) u) K% E5 Y. @3 b3 m - }
复制代码 EVM vs TON在以太坊虚拟机(EVM)中,重放保护并不在讨论范畴之内的: 私钥只有一个相关地址 每次处理交易后,nonce 会自动增加 钱包所有者支付 GAS,失败的交易无法重放 ) p- n/ x7 r: L
TON 更为灵活: 一个私人密钥可控制多个钱包 钱包可编程
% ]; K1 ^) ` Q6 t4 j0 J, q 建议幸运的是,对于大多数合约开发人员来说,有一个简单的建议:避免处理外部消息。 让 "钱包 "合约来完成这项工作,在你的合约中只包含 recv_internal。 即使您编写的是 "多重签名 "或特殊钱包,您仍然可以只处理来自普通钱包的内部信息。 在这种情况下,您将获得 EVM 风格的交易,并拥有底层的所有灵活性。
$ g+ ~3 t- n- q: d% J! f ' v5 v; s$ k& Y. L; z; p3 C+ d
, \) Z0 {) d2 J' n4 h/ b
|