💎 欢迎来到第 8 章。 本章将探讨与 TON 中区块链安全和管理相关的各种主题。本章首先深入研究了简单的钱包实现、重放攻击、拒绝服务攻击和合约建议。之后,重点将转移到消息流上,指导你掌握有效使用消息流的必要知识和技能。TON 中的GAS管理是其中一课涉及的另一个重要方面,它使您能够理解并实施高效GAS管理策略。此外,本章还包括一个深入探讨 TON 存储管理的教程,为您提供有效数据存储的基本见解和技术。 您可能已经知道,TON 智能合约有三个可能的入口: 每个合约都必须有一个 recv_internal 函数。 可选的 recv_external 用于接收外部信息。 run_ticktock 在特殊智能合约的 ticktock 交易中调用。普通合约不使用。
, E0 b1 n: |: P" d% w1 |
下面我们将讨论 recv_external。这个概念很熟悉:用户通常在链外进行交易,用自己的私钥签名,然后发送给合约。 简单的钱包实施TON 中的钱包只是一个智能合约。让我们写一个简单的钱包,它只转发用我们的私钥签名的信息。基本实现如下: - ;; the only function that can get messages not from other smart contracts* c& Y3 Y- c+ ~
- () recv_external(slice in_msg) impure { 8 r+ A# {1 Q8 L; @
- var signature = in_msg~load_bits(512); ;; read the signature from the incoming message2 v' N' B, `5 L) w, _, a0 A
- var cs = in_msg; ;; make a copy of the incoming message
9 x" M) \& `; o: J* D2 S - var ds = get_data().begin_parse(); ;; reading the contract storage
# A3 m2 ~" [# t8 n - var public_key = ds~load_uint(256); ;; read the authorized key (contract owner)6 @# ~* i$ I* R4 T+ ~# ~! E# M
- throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));; }2 b( k& G7 u
- accept_message(); ;; we are ready to pay for gas to process this, e" |& @0 z' C* I' \ E8 J# [
- var mode = cs~load_uint(8); ;; read the mode from incoming message
8 K+ X. e, _ E b1 W- B" E - send_raw_message(cs~load_ref(), mode); ;; send the provided message
: R3 |: k2 y4 O5 W' T s - }
复制代码事实上,该合约将处理合约所有者签署的信息。但没有什么能阻止恶意方读取有效载荷并重新传输。这就是所谓的 "重放攻击"。 重复一遍。攻击者获取合约所有者签署的信息,并再次发送到这个钱包。它将再次被执行。 重放攻击钱包所有者现在需要提供一个时间戳,直到信息有效为止。但在这段时间内,信息仍可被重放。 - () recv_external(slice in_msg) impure {
% T! q1 i. N' L& u - var signature = in_msg~load_bits(512);; z# Y$ `2 V, ?- @- ]. t. _
- var cs = in_msg; ' K1 U: z( c8 v8 v K! J$ c. j
- var valid_until = cs~load_uint(32); ;; added, R j& |" n$ R) Y O8 O6 m. v2 Q
- throw_if(35, valid_until <= now()); ;; added2 K0 X( R& e+ y4 ^9 w0 \
- var ds = get_data().begin_parse();
" n: P) a* P4 R+ i, N' x @ - var public_key = ds~load_uint(256);
# I/ n/ k0 W1 |$ Q6 u0 y! A- K5 c - throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));" V7 V! J, v$ ^$ S2 x& s, C
- accept_message();, A% w/ H/ a+ A4 G. x$ J
- var mode = cs~load_uint(8);
3 o- L6 A* v0 C. D1 f& y2 K, | - send_raw_message(cs~load_ref(), mode);0 e3 [$ p8 |; @. U
- }
复制代码 重放攻击钱包所有者现在需要提供一个序列号,该序列号会在存储的序列号上递增。这确保了任何信息都只能被处理一次: - () recv_external(slice in_msg) impure {
2 I2 Y- a: Q& v* T" o - var signature = in_msg~load_bits(512);
. ~8 @1 C! o; E - var cs = in_msg;
M0 G: U" Y) z0 _: m o. h, X - var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32)); ;; changed: @7 {5 X' o. b' x+ E, G; ?, U
- throw_if(35, valid_until <= now());
/ B, b2 a7 Q3 w: w! t6 E7 p - var ds = get_data().begin_parse();
- J4 e- h/ A! {6 \0 e! H: g- p - var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256)); ;; changed
0 O8 N; f1 c4 \, k/ n; b g - throw_unless(33, msg_seqno == stored_seqno); ;; added
) b/ Q$ ~6 r' W3 B - throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
- f8 g2 O8 I7 |/ V8 n - accept_message();8 k$ P6 u, `& t+ j0 V9 K
- var mode = cs~load_uint(8);
0 I, H7 I# P3 H+ ]! [, | - send_raw_message(cs~load_ref(), mode);
) D8 \' B" j3 W
+ U( @, N I) t# @4 [5 a- set_data(begin_cell() ;; added
& u3 e6 ~# ?9 ?6 x$ f9 [- X# {" A2 V - .store_uint(stored_seqno + 1, 32)
) o. s; u3 {. _2 k) @7 y3 @) H4 Q) x - .store_uint(public_key, 256)+ j* \; o4 F# u5 q2 r
- .end_cell());
, w0 x7 v' F g& N4 {2 z - }
复制代码 拒绝服务攻击应谨慎使用 recv_external,并在验证后才使用 accept_message()。否则,GAS可能会因GAS恶意执行而耗尽合约资金 - () recv_external(slice in_msg) impure {
( j) H6 ]$ t4 ?( R2 \6 U - var signature = in_msg~load_bits(512);
: ?3 @5 E& z2 ?6 I8 k1 O: Z" M6 V - var cs = in_msg; " ^7 O3 H) Z( L- L- z t& S
- var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32));
P) ^! i/ q/ \8 p+ f( a( r* e$ W - throw_if(35, valid_until <= now());( l; v. a) n! p x4 P# J
- var ds = get_data().begin_parse();
3 I& B$ E5 x8 x& J4 _1 U, p - var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256)); . ~$ q5 w( B# [ D: k9 n
- throw_unless(33, msg_seqno == stored_seqno);
$ D$ w( e5 M; a( p" E; v6 m - throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
& @. Y6 A& i4 ?4 w0 e! {+ ~4 h - 5 Y7 j- U$ `2 w; ]6 ?' L8 H
- accept_message(); ;; accept after checking signature only1 {3 Q! Z% U& n1 e/ V; f! `! N
- / L& p: K- V& q3 m7 d2 s. j7 V# N
- var mode = cs~load_uint(8);
! U- \5 Q6 t- r - send_raw_message(cs~load_ref(), mode);
% O+ x4 y# r, `7 X- x" X! H$ V - set_data(begin_cell()2 e7 @& x, w- X) V' l- Y
- .store_uint(stored_seqno + 1, 32)
' n; D( |4 u9 @ - .store_uint(public_key, 256)
& h5 F( r- H c8 w - .end_cell());
+ F6 D/ h- F, C1 a- L0 r3 @ - }
复制代码另外请注意,同一签名的信息可以在同一所有者的另一个钱包上重放。或者,测试网络中的信息可以在主网络中重放。
, A' e% Z$ K, p6 r失败的重放攻击请注意,如果在 accept_message() 后出现错误,交易将被写入区块链,费用将从合约余额中扣除,但不会更新存储,也不会像任何有错误退出代码的交易那样执行操作。因此,如果合约接受了一条外部消息,然后由于消息数据出错或发送了一条序列化错误的消息而抛出异常,它将支付处理费用,但却无法防止消息重放。合约会一次又一次地接受相同的消息,直到耗尽全部余额。 这就是为什么我们要在信息解析和执行前保存更新状态的原因: - () recv_external(slice in_msg) impure {
9 n/ @; I: r* I& C4 ]! q; r* W - var signature = in_msg~load_bits(512);
1 T0 w: J' M3 ]" L5 \7 X. s$ t$ s - var cs = in_msg; 2 [& n, @( Y- q- I- L( F) a$ C4 |
- var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32));
* t1 D0 b$ m6 J, h0 _! \) J - throw_if(35, valid_until <= now());
* N" c8 G# a( q( C - var ds = get_data().begin_parse();
7 E0 m1 T" I: ~# s/ }3 s - var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256)); $ @# H+ T) N, F$ S2 n2 m6 v: k+ D
- throw_unless(33, msg_seqno == stored_seqno);
2 u3 H6 V: A! G! W2 ?# z - throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));+ _0 b! l2 ^3 T; g2 c
- accept_message();
4 `+ o7 V) W n( l& [+ B
, R; b [0 C ?5 N! m- set_data(begin_cell() ;; moved% q8 ?1 t" {$ ~7 |- `
- .store_uint(stored_seqno + 1, 32)
9 N) v' `9 G5 } n5 S - .store_uint(public_key, 256)7 y5 ]9 E0 {0 K3 K8 R- F' \
- .end_cell());
4 r0 M; _& {& F: t4 I! ^ - commit(); ;; added6 A3 r2 ~9 |( U8 t* a3 F1 V$ B) g
8 v! t) r3 e% R. C- var mode = cs~load_uint(8); ;; can fail) K8 H" S7 k( x+ L% Z" m
- send_raw_message(cs~load_ref(), mode); ;; can fail
' t( y X: i4 J7 q- e - }
复制代码 EVM vs TON在以太坊虚拟机(EVM)中,重放保护并不在讨论范畴之内的: 私钥只有一个相关地址 每次处理交易后,nonce 会自动增加 钱包所有者支付 GAS,失败的交易无法重放 9 o- P$ M7 D. b4 i; b! t
TON 更为灵活: 一个私人密钥可控制多个钱包 钱包可编程
+ V% W0 Y! C4 x% [* m0 b 建议幸运的是,对于大多数合约开发人员来说,有一个简单的建议:避免处理外部消息。 让 "钱包 "合约来完成这项工作,在你的合约中只包含 recv_internal。 即使您编写的是 "多重签名 "或特殊钱包,您仍然可以只处理来自普通钱包的内部信息。 在这种情况下,您将获得 EVM 风格的交易,并拥有底层的所有灵活性。
& \9 J1 L' J, u- n! h5 D + k' t. w: f, J W1 @
# O- `6 t4 T# {) f, g' a, Y* D( Y# T |