💎 欢迎来到第 8 章。 本章将探讨与 TON 中区块链安全和管理相关的各种主题。本章首先深入研究了简单的钱包实现、重放攻击、拒绝服务攻击和合约建议。之后,重点将转移到消息流上,指导你掌握有效使用消息流的必要知识和技能。TON 中的GAS管理是其中一课涉及的另一个重要方面,它使您能够理解并实施高效GAS管理策略。此外,本章还包括一个深入探讨 TON 存储管理的教程,为您提供有效数据存储的基本见解和技术。 您可能已经知道,TON 智能合约有三个可能的入口: 每个合约都必须有一个 recv_internal 函数。 可选的 recv_external 用于接收外部信息。 run_ticktock 在特殊智能合约的 ticktock 交易中调用。普通合约不使用。 % O/ B6 [# U6 z$ \* O2 `2 E
下面我们将讨论 recv_external。这个概念很熟悉:用户通常在链外进行交易,用自己的私钥签名,然后发送给合约。 简单的钱包实施TON 中的钱包只是一个智能合约。让我们写一个简单的钱包,它只转发用我们的私钥签名的信息。基本实现如下: - ;; the only function that can get messages not from other smart contracts% P+ w9 m) q% Y( W
- () recv_external(slice in_msg) impure {
; _* V' n k$ W$ t# n" s - var signature = in_msg~load_bits(512); ;; read the signature from the incoming message/ F x7 C) r/ ~8 q
- var cs = in_msg; ;; make a copy of the incoming message
# k) T |" D u- Q2 B1 v; p. ~; w( } - var ds = get_data().begin_parse(); ;; reading the contract storage
' a. {; b) @$ W - var public_key = ds~load_uint(256); ;; read the authorized key (contract owner)
/ b+ a* w/ c/ d2 p8 G: Q ^) q) q - throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
% a2 H8 q1 u% d; _ - accept_message(); ;; we are ready to pay for gas to process this, a! c" Z# ^9 L9 w
- var mode = cs~load_uint(8); ;; read the mode from incoming message
% T% I! G2 ?. l9 N, ?7 S - send_raw_message(cs~load_ref(), mode); ;; send the provided message
; g3 I& h% w! L4 ?/ w - }
复制代码事实上,该合约将处理合约所有者签署的信息。但没有什么能阻止恶意方读取有效载荷并重新传输。这就是所谓的 "重放攻击"。 重复一遍。攻击者获取合约所有者签署的信息,并再次发送到这个钱包。它将再次被执行。 重放攻击钱包所有者现在需要提供一个时间戳,直到信息有效为止。但在这段时间内,信息仍可被重放。 - () recv_external(slice in_msg) impure { f' R0 H w7 w1 Z+ q
- var signature = in_msg~load_bits(512);
5 T0 E! n: I7 i5 N - var cs = in_msg;
' V( w( h4 z1 |& @& {9 x! Y - var valid_until = cs~load_uint(32); ;; added
, \& K9 U% O! B1 K; x5 L/ z - throw_if(35, valid_until <= now()); ;; added
2 k: d! B0 K- R/ u: `$ M: x6 ? - var ds = get_data().begin_parse();$ D% C$ v0 ~ n7 W
- var public_key = ds~load_uint(256); d0 x3 D; Q) t! g- Q8 t
- throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
" h5 S' s8 p; y4 b; C/ I - accept_message();
5 m& n5 Z( |* m1 f4 Z$ z( M0 z) D - var mode = cs~load_uint(8);
8 T$ I! ?4 _# F2 {) ? - send_raw_message(cs~load_ref(), mode);: f0 k, n2 T& `& C5 _ c0 }
- }
复制代码 重放攻击钱包所有者现在需要提供一个序列号,该序列号会在存储的序列号上递增。这确保了任何信息都只能被处理一次: - () recv_external(slice in_msg) impure {
5 B0 b: G& O0 m1 ] - var signature = in_msg~load_bits(512);. U* t. A, j$ p* y
- var cs = in_msg;
9 o+ D1 c2 c) e - var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32)); ;; changed
/ E! a0 B8 y' S, W - throw_if(35, valid_until <= now());
8 [8 Y; Z+ [, N9 e - var ds = get_data().begin_parse(); " ? {5 W H2 e2 `$ _# g. G
- var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256)); ;; changed
! W/ m+ Q" M, O! Y; z: H0 q: }! \ - throw_unless(33, msg_seqno == stored_seqno); ;; added, W* o& O+ e, U
- throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
: Z( M+ }; x# p3 r+ J" I& f - accept_message();# B- k" [& b+ P4 S( u: T
- var mode = cs~load_uint(8);
. V- W' p6 a! L( q. P0 i) [5 g% S - send_raw_message(cs~load_ref(), mode);) b# M; F2 v! i3 {. [% |
- 0 j. _" v1 `% Q! K
- set_data(begin_cell() ;; added
2 q2 |9 w6 X! G3 Y1 y/ x - .store_uint(stored_seqno + 1, 32)
( m+ H: o$ {# Z' |4 ^ - .store_uint(public_key, 256)
8 q& S6 z1 z# Q9 c1 W, J" ^3 | - .end_cell()); 6 \! F: z2 N1 n& k
- }
复制代码 拒绝服务攻击应谨慎使用 recv_external,并在验证后才使用 accept_message()。否则,GAS可能会因GAS恶意执行而耗尽合约资金 - () recv_external(slice in_msg) impure {
/ V) j3 k- `' j1 _" O5 m! N - var signature = in_msg~load_bits(512);
8 ^9 H& n, d( t) s( I$ w3 ` - var cs = in_msg;
5 T$ _" S5 P8 m% U - var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32)); : ]& u3 {$ c$ Z n) R
- throw_if(35, valid_until <= now());
# L5 y+ h O6 H) d' x+ I - var ds = get_data().begin_parse();
- g: v; V/ K ]% K; C% v- ? - var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256)); 9 X8 W9 a4 Z% ~. {' T" U8 Z" S0 G
- throw_unless(33, msg_seqno == stored_seqno); 2 c$ B- s* Z- @
- throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));$ N5 }% L/ i+ e& }' x Y) N, d
- 1 F, L( L- p' v. `
- accept_message(); ;; accept after checking signature only( V) `" S/ P; P, u6 _$ p3 {
6 i' x# r) H& F( O! E- var mode = cs~load_uint(8);
1 ?; w1 o; @6 j: C" q - send_raw_message(cs~load_ref(), mode);
u4 b# H6 Y9 y+ W% J" } - set_data(begin_cell() Q% o& Z h1 q1 |8 E" J5 c
- .store_uint(stored_seqno + 1, 32)
( P: X6 c ~+ k: m2 Q! g - .store_uint(public_key, 256)
9 ]2 ^; I/ I9 l! m g& c - .end_cell());
) X$ F! X$ _$ Z: N6 @9 I' U - }
复制代码另外请注意,同一签名的信息可以在同一所有者的另一个钱包上重放。或者,测试网络中的信息可以在主网络中重放。
( l/ m' [/ C" b9 j5 c0 y失败的重放攻击请注意,如果在 accept_message() 后出现错误,交易将被写入区块链,费用将从合约余额中扣除,但不会更新存储,也不会像任何有错误退出代码的交易那样执行操作。因此,如果合约接受了一条外部消息,然后由于消息数据出错或发送了一条序列化错误的消息而抛出异常,它将支付处理费用,但却无法防止消息重放。合约会一次又一次地接受相同的消息,直到耗尽全部余额。 这就是为什么我们要在信息解析和执行前保存更新状态的原因: - () recv_external(slice in_msg) impure {
( T' }4 h" b, p( m8 ]3 i - var signature = in_msg~load_bits(512);
0 e$ [7 k }; j+ K - var cs = in_msg; 3 j6 O& g5 G, C9 ` V
- var (msg_seqno, valid_until) = (cs~load_uint(32), cs~load_uint(32)); # a" n4 i C& q# G
- throw_if(35, valid_until <= now());5 y3 e+ E7 V: Y1 p1 z& G# a
- var ds = get_data().begin_parse();
- ?6 k9 \" s7 L/ L - var (stored_seqno, public_key) = (ds~load_uint(32), ds~load_uint(256));
9 c# ^' Y) {; d7 \/ ]4 d - throw_unless(33, msg_seqno == stored_seqno);
+ o( {$ b/ G8 _: k4 C# H) U - throw_unless(34, check_signature(slice_hash(in_msg), signature, public_key));
; M3 W ?- _, R9 w$ z4 M# O2 P - accept_message();
: @6 B5 z6 Y. o" X( Z6 L8 {
+ l7 r- `9 N9 q& c- set_data(begin_cell() ;; moved
4 h2 x1 A1 r8 w' b- b% Q4 O: { - .store_uint(stored_seqno + 1, 32)
- z/ L% \4 D9 X7 X# g - .store_uint(public_key, 256)$ g: X0 w! s2 X5 {
- .end_cell());- c$ |# @5 U7 |3 g+ D5 E/ A
- commit(); ;; added
5 t8 ?6 d; E3 m5 f7 S6 A: o - ! e1 e. g2 I d, B% m$ V
- var mode = cs~load_uint(8); ;; can fail# k& d2 j2 [7 C
- send_raw_message(cs~load_ref(), mode); ;; can fail6 b* O4 b* R7 Q( Q! N
- }
复制代码 EVM vs TON在以太坊虚拟机(EVM)中,重放保护并不在讨论范畴之内的: 私钥只有一个相关地址 每次处理交易后,nonce 会自动增加 钱包所有者支付 GAS,失败的交易无法重放
; [. H0 \/ o8 o M
TON 更为灵活: 一个私人密钥可控制多个钱包 钱包可编程 - 1 ^2 }9 [+ A. u4 o3 ]/ H
建议幸运的是,对于大多数合约开发人员来说,有一个简单的建议:避免处理外部消息。 让 "钱包 "合约来完成这项工作,在你的合约中只包含 recv_internal。 即使您编写的是 "多重签名 "或特殊钱包,您仍然可以只处理来自普通钱包的内部信息。 在这种情况下,您将获得 EVM 风格的交易,并拥有底层的所有灵活性。 & J% \; U8 a( y, B0 z8 g. c- K' S# A1 P
/ W# ?3 a: O7 V' T+ v
+ B0 A: F1 m8 x; c i' B: H |