💎 欢迎来到第 8 章。 本章将探讨与 TON 中区块链安全和管理相关的各种主题。本章首先深入研究了简单的钱包实现、重放攻击、拒绝服务攻击和合约建议。之后,重点将转移到消息流上,指导你掌握有效使用消息流的必要知识和技能。TON 中的GAS管理是其中一课涉及的另一个重要方面,它使您能够理解并实施高效GAS管理策略。此外,本章还包括一个深入探讨 TON 存储管理的教程,为您提供有效数据存储的基本见解和技术。 您可能已经知道,TON 智能合约有三个可能的入口: 每个合约都必须有一个 recv_internal 函数。 可选的 recv_external 用于接收外部信息。 run_ticktock 在特殊智能合约的 ticktock 交易中调用。普通合约不使用。 7 W9 l1 O8 ~; p* h
下面我们将讨论 recv_external。这个概念很熟悉:用户通常在链外进行交易,用自己的私钥签名,然后发送给合约。 简单的钱包实施TON 中的钱包只是一个智能合约。让我们写一个简单的钱包,它只转发用我们的私钥签名的信息。基本实现如下: - ;; the only function that can get messages not from other smart contracts
- x( f+ r$ d1 l8 d5 q$ @- i - () recv_external(slice in_msg) impure {
0 G' b/ E7 A. ?9 `* ]# d$ ?- P - var signature = in_msg~load_bits(512); ;; read the signature from the incoming message
& m$ Y% }# [ M9 d' | - var cs = in_msg; ;; make a copy of the incoming message
9 }! P# _! M ?$ P+ `( B - var ds = get_data().begin_parse(); ;; reading the contract storage
! F( ~# }& c, H3 j0 v - var public_key = ds~load_uint(256); ;; read the authorized key (contract owner)6 K. o9 _9 X4 h, x
- throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));' P* h, [) ~* k* v2 t
- accept_message(); ;; we are ready to pay for gas to process this( j. ]" K, k6 k
- var mode = cs~load_uint(8); ;; read the mode from incoming message3 [% ~5 b; v' ]
- send_raw_message(cs~load_ref(), mode); ;; send the provided message# ?6 }. d# Q7 `
- }
复制代码事实上,该合约将处理合约所有者签署的信息。但没有什么能阻止恶意方读取有效载荷并重新传输。这就是所谓的 "重放攻击"。 重复一遍。攻击者获取合约所有者签署的信息,并再次发送到这个钱包。它将再次被执行。 重放攻击钱包所有者现在需要提供一个时间戳,直到信息有效为止。但在这段时间内,信息仍可被重放。 - () recv_external(slice in_msg) impure {
3 ?' B. Y) w3 b - var signature = in_msg~load_bits(512);# X4 Q2 t8 A* i$ d, j5 g
- var cs = in_msg;
1 K, F; f" L& \7 G - var valid_until = cs~load_uint(32); ;; added2 @& D, l ]& [7 [$ Z
- throw_if(35, valid_until <= now()); ;; added
5 I2 U6 h/ T' o; s8 N9 ~ - var ds = get_data().begin_parse();
5 o- a9 ?1 v, f0 f - var public_key = ds~load_uint(256);
! i& z. F, d/ d x! ]3 u6 f* W# `& | - throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
8 Z2 P4 E8 H; S0 }# G - accept_message();6 f% Q' f9 c; @
- var mode = cs~load_uint(8);
9 L3 m( o. e4 K7 v - send_raw_message(cs~load_ref(), mode);
6 ^& Y0 `% |+ q2 u# ?1 H) U* ~ - }
复制代码 重放攻击钱包所有者现在需要提供一个序列号,该序列号会在存储的序列号上递增。这确保了任何信息都只能被处理一次: - () recv_external(slice in_msg) impure {+ C: I, G6 m4 X1 k5 M1 P7 L- A$ M" E
- var signature = in_msg~load_bits(512);6 k# T3 y J) T7 o9 Z
- var cs = in_msg;
* E+ m! R& ^* N( U - var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32)); ;; changed w1 ~8 a# ^" I% }9 {
- throw_if(35, valid_until <= now());
/ d, t5 T3 V) m s3 o - var ds = get_data().begin_parse();
' ^. S* W: `0 C - var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256)); ;; changed4 i+ N9 v/ {8 z
- throw_unless(33, msg_seqno == stored_seqno); ;; added
$ U3 ^' j/ ?5 U. Z) e; V2 ~ - throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
% E5 U" F n3 @; n" T& i - accept_message();
4 Q. G/ ^) ^" ?, k9 P/ P - var mode = cs~load_uint(8);: g& j& @3 C' I% N2 M
- send_raw_message(cs~load_ref(), mode);
. {+ n, N8 Z' y6 q - 7 p. i& Q: Q- c0 e6 p( R/ ~$ t G
- set_data(begin_cell() ;; added7 L, T0 u2 m: X# q% u
- .store_uint(stored_seqno + 1, 32)
+ d/ E p: K( U7 K - .store_uint(public_key, 256)
- s! F; V; l' v$ B+ C - .end_cell()); y% B" y2 n: O) a4 J
- }
复制代码 拒绝服务攻击应谨慎使用 recv_external,并在验证后才使用 accept_message()。否则,GAS可能会因GAS恶意执行而耗尽合约资金 - () recv_external(slice in_msg) impure {
+ E/ P" |4 J1 U+ D! J - var signature = in_msg~load_bits(512);
& U* G3 h1 m5 m% j! I0 x: u - var cs = in_msg; 7 h% Q3 I, R0 |+ J
- var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32)); 2 C: K. t0 ^4 h0 u! [( y3 _* n
- throw_if(35, valid_until <= now());# k4 |/ y) U# b" a4 F. M
- var ds = get_data().begin_parse(); , b! x/ z0 \, @1 R$ l' V
- var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256));
6 V2 @8 Q4 _; H. p - throw_unless(33, msg_seqno == stored_seqno); ; L) Z- Q. L4 ~: a5 W# y6 q! b
- throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
, L/ `0 e( _$ @$ V
( J9 v, P$ K0 l; Y: j- accept_message(); ;; accept after checking signature only
`0 N6 }$ c4 C9 S - 6 B& B* ~2 p) e0 @ `7 g8 R
- var mode = cs~load_uint(8);% m* C, L+ N; V8 F( o- K
- send_raw_message(cs~load_ref(), mode);0 ~* |) h4 T2 @' E
- set_data(begin_cell()
: w7 g0 Q+ \( {( Q9 V7 U - .store_uint(stored_seqno + 1, 32)8 ^- C# v+ i, Y- j
- .store_uint(public_key, 256)" g! M0 R- _" n
- .end_cell());
# G3 J6 l& B, k `% |+ g8 z - }
复制代码另外请注意,同一签名的信息可以在同一所有者的另一个钱包上重放。或者,测试网络中的信息可以在主网络中重放。 0 c1 j5 q" k' D) k7 m% w" X
失败的重放攻击请注意,如果在 accept_message() 后出现错误,交易将被写入区块链,费用将从合约余额中扣除,但不会更新存储,也不会像任何有错误退出代码的交易那样执行操作。因此,如果合约接受了一条外部消息,然后由于消息数据出错或发送了一条序列化错误的消息而抛出异常,它将支付处理费用,但却无法防止消息重放。合约会一次又一次地接受相同的消息,直到耗尽全部余额。 这就是为什么我们要在信息解析和执行前保存更新状态的原因: - () recv_external(slice in_msg) impure {% b3 r8 w7 V T5 f/ }
- var signature = in_msg~load_bits(512);6 ]% G% [( E# r* k
- var cs = in_msg; 2 T, y4 v! o/ D* ?3 U& _
- var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32));
* }# F7 L# O7 Z1 Z* H- v - throw_if(35, valid_until <= now());* d; F8 B& @# h2 _9 V+ B
- var ds = get_data().begin_parse(); + n* h2 |1 H9 J2 Z0 R' O- o
- var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256)); 6 \9 K/ O. e% ] f7 w4 q* \9 f
- throw_unless(33, msg_seqno == stored_seqno);
1 [! i+ r9 A" Z: w: V - throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
, U3 M! `% ^3 _( P7 i# A" v( t - accept_message();
# F! P! p5 }: `" ~3 t. u" P; s
; c: f& h) A U! \" |$ y- set_data(begin_cell() ;; moved
' ^: A3 g/ t- R - .store_uint(stored_seqno + 1, 32)
. ]9 w* y O, P8 _3 [$ s. J - .store_uint(public_key, 256): K3 x4 R9 T# E" _# |, D7 V5 |
- .end_cell());8 F% g# _% A1 \4 R6 I
- commit(); ;; added
& j0 S) K" C$ w, o: ]
4 q3 E5 o2 S6 ~( A. a9 Q- var mode = cs~load_uint(8); ;; can fail
$ p. e' A8 C# |( T - send_raw_message(cs~load_ref(), mode); ;; can fail4 _! H( l% k: k+ G# K9 W* Z0 q+ Y
- }
复制代码 EVM vs TON在以太坊虚拟机(EVM)中,重放保护并不在讨论范畴之内的: 私钥只有一个相关地址 每次处理交易后,nonce 会自动增加 钱包所有者支付 GAS,失败的交易无法重放 # Z& d" y: N" J3 ?$ N, i
TON 更为灵活: 一个私人密钥可控制多个钱包 钱包可编程 - ' Q7 s6 W# r( U0 B
建议幸运的是,对于大多数合约开发人员来说,有一个简单的建议:避免处理外部消息。 让 "钱包 "合约来完成这项工作,在你的合约中只包含 recv_internal。 即使您编写的是 "多重签名 "或特殊钱包,您仍然可以只处理来自普通钱包的内部信息。 在这种情况下,您将获得 EVM 风格的交易,并拥有底层的所有灵活性。
+ S9 T& |' E3 }0 ^
3 M/ T" C/ y% A/ T( o) i
- K# \5 ^4 l. a# z" V0 @) V8 \ |