
Chao xìn mọi người lại là mình đây Nhabachoc KAD.
Bài viết này có dành cho bạn?
Chưa thoả mãn với những kiến thức ở bài Solidity cơ bản
Chưa hiểu rõ tại sao smart contract lại an toàn, minh bạch
Hành trang cần chuẩn bị:
Kiến thức về Solidity cơ bản
Cùng tắt đèn bật ý tưởng, gét go 🪄
Ethereum Virtual Machine - EVM
Máy ảo (VM) là một chương trình chạy trên hệ điều hành gốc. Nó mô phỏng một máy vật lý và cho phép nó chạy cùng một nền tảng trên nhiều hệ điều hành và kiến trúc phần cứng.
Có hai loại kiến trúc máy ảo chính:
Máy ảo register-based <các thanh ghi>
Máy ảo stack-based <mô hình ngăn xếp>
Hai điểm khác biệt chính giữa hai kiến trúc này là:
Cách mỗi VM lưu trữ, truy xuất và sử dụng các đối số (ví dụ: toán hạng cho các phép toán số học) khi thực hiện các lệnh được xác định bởi ngôn ngữ trình biên dịch mã.
Cấu trúc dữ liệu để lưu trữ các đối số này.
Máy ảo register-based
VM này sử dụng các thanh ghi để lưu trữ toán hạng. Cấu trúc dự liệu:
Key : Địa chỉ thanh ghi
Value : Giá trị muốn lưu trữ
Một số VM register-based nổi tiếng: Erlang VM, Lua VM, Dalvik VM
Máy ảo stack-based
VM này sử dụng stack <ngăn xếp> làm cấu trúc dữ liệu để lưu trữ các toán hạng. Các hoạt động được thực hiện bằng cách lấy dữ liệu từ stack, xử lý và đẩy kết quả trở lại stack. Chúng thường được thực hiện bằng cách sử dụng PUSH
và POP.
Sử dụng cơ chế LIFO có nghĩa là phần tử cuối cùng được đẩy vào stack luôn là phần tử đầu tiên được lấy ra khỏi stack.
Một số VM register-based nổi tiếng: Java VM
⇒ EVM là một máy ảo dựa trên Máy ảo stack-based.
Stack
Stack được sử dụng mọi lúc trong Solidity khi chúng ta gán giá trị cho biến. Có 5 quy tắc chính giúp bạn hiểu thực sự smart contract:
2 quy tắc liên quan đến stack:
Có chi phí tương tác với dự liệu thấp nhất.
Stack chỉ có hiệu lực trong function scope.
3 quy tắc về thao tác dữ liệu stack:
EVM sử dụng 4 mã lệnh cơ bản để thao tác với stack:
PUSH
,POP
,SWAP
,DUP
.Các đối số của opcode khác luôn lấy dữ liệu trên cùng trong stack.
Các opcode liên quan đến
storage
,memory
vàcalldata
tải các giá trị từ các vị trí dữ liệu này vào stack bằng cách sử dụng các opcodes liên quan đến từng vị trí dữ liệu.
Cách thức lưu trữ
Giá trị của 1 giá trị trong stack 256 bit.
Giới hạn về kích thước của stack là 1.024 giá trị <có trong tài liệu Ethereum Yellow Paper>.
Chỉ có thể truy cập các mục trên cùng thứ 16 trong stack.
Calldata
4 byte đầu tiên tương ứng với bộ chọn của chữ ký hàm.
các byte còn lại tương ứng với các tham số đầu vào của hàm. Mỗi đối số đầu vào luôn dài 32 byte. Nếu loại của nó nhỏ hơn 32 byte, đối số sẽ được đệm.
Lưu ý : Các đối số đầu vào được đệm ở bên phải hoặc bên trái tùy thuộc vào loại của chúng (ví dụ: uintN
hoặc address
được đệm ở bên trái, trong khi bytesN
được đệm ở bên phải).
Dữ liệu được gửi cùng 1 messeage call. Nó đề cập đến các byte hex thô
Calldata được tạo từ các byte được sắp xếp liên tục giống như memory trái ngược với storage hoặc stack được tạo thành words (32 byte). Để đọc được calldata giống như memory: có thể tải 32 byte mỗi lần ( mload
với memory và calldataload
với calldata). Tuy nhiên, nó hoạt động khác với memory vì bạn không thể ghi vào nó.
Một cách để phân biệt calldata với memory là “ai tạo dữ liệu trong calldata” và “ai tạo dữ liệu trong bộ nhớ”:
Một cách tốt để suy nghĩ về sự khác biệt giữa
calldata
vàmemory
và cách chúng nên được sử dụng làcalldata
được tạo bởi người gọi, trong khimemory
được tạo bởi người được gọi.
Bây giờ chúng ta hãy xem các tính năng chính của calldata. Calldata là một vị trí dữ liệu nơi lưu giữ tham số dữ liệu của một giao dịch hoặc cuộc gọi.
Không thể sửa đổi - chỉ đọc: Bạn không thể sửa đổi hoặc thay đổi dữ liệu trong calldata. Nó không thể được ghi đè.
Hầu như không giới hạn về kích thước
Rất rẻ và tiết kiệm gas: Đọc + cấp phát byte trong calldata rất rẻ và tiết kiệm gas.
4 gas cho byte
0x00
16 gas cho các byte khác không.
opcode
CALLDATALOAD
chỉ tốn 3 gas để đọc một từ 32 byte
Không liên tục (sau khi giao dịch đã hoàn thành)
Cụ thể cho các giao dịch và cuộc gọi hợp đồng.
Storage
Như các bạn biết Storage là kho lưu trữ dữ liệu lâu dài và vô cùng tốn kém:
Khởi tạo vị trí lưu trữ: Bộ nhớ ban đầu là → muốn lưu trữ giá trị khác 0 tốn 20.000 gas
Chỉnh sửa giá trị tại 1 vị trí lưu trữ tốn 5.000 gas
Xoá giá trị tại vị trí lưu trữ sẽ hoàn lại 15.000 gas
Câu hỏi:
Đọc dữ liệu từ Storage có tốn gas không?
Việc đọc dữ liệu từ Storage thường thấy thì không tốn gas nhưng chỉ miễn phí với tài khoản EOA, tính phí khi thao tác đọc dữ liệu là một phần của giao dịch có sửa đổi trạng thái trên hợp đồng.
Hợp đồng này có thể đọc dữ liệu từ Storage của hợp đồng khác không?
Mặc định smart contract chỉ có thể đọc dữ liệu của chính nó trong môi trường thực thi. Một smart contract có thể đọc dữ liệu của hợp đồng khác khi có các chức năng đọc trong ABI.
Slot hay còn gọi là vị trí lưu trữ trong smart contract có đặc điểm:
Tổng cộng 2^256 slot nhớ để đọc/ghi
Mỗi slot có kích thước lên đến 256 bit (32 byte)
Value mặc định của mỗi slot là 0
Cách thức lưu trữ
Quy tắc chung khi lưu trữ:
Storage chỉ lưu các biến, không lưu constant
Các biến lần lượt được vào slot theo thứ tự lower-order (nghĩa là từ phải qua trái).
Nếu size của biến vượt quá size còn lại của slot, biến này sẽ được đưa qua slot mới.
Fixed size
Việc lưu trữ dữ liệu là ánh xạ key-value trong cơ sở dữ liệu:
Key : Biến đó đang nằm trong Slot nào, key chính là Slot
Value : Giá trị thực ở Slot này
Dynamic Size
1.1. Array
Array sẽ luôn chiếm 1 slot mới kể cả khi slot liền kề còn trống
Vẫn là theo dạng key-value nhưng slot đầu tiên dùng để chứa độ dài array gọi là
A
:Key : Biến đó đang nằm trong Slot nào, key chính là Slot
Value : Length của array
Chi tiết các phần tử trong array sẽ chiếm từ slot thứ tự: keccak256(<Slot
A
ở dạng chuỗi 64 ký tự hex>)1.2. Mappings
Mapping sẽ luôn chiếm 1 slot mới
Vẫn là theo dạng key-value nhưng slot đầu tiên không chứa giá trị gọi là
M
:Key : Biến đó đang nằm trong Slot nào, key chính là Slot
Value : Không chứa giá trị
Các phần từ trong mapping sẽ chiếm slot: keccak256(hex(<giá trị key của mapping ở dạng chuỗi 64 ký tự hex>) + hex(Slot
M
ở dạng chuỗi 64 ký tự hex))Bytes và String
3.1. Bytes
3.2. String
String sẽ luôn chiếm 1 slot mới
String < 32 byte: string được lưu trữ kèm với độ dài của nó theo quy tắc: chuỗi được lưu từ bit trái qua phải (higher-order), còn độ dài thì lưu từ bit phải qua trái (lower-order) với giá trị
length * 2
.String > 32 byte: thì main slot sẽ lưu trữ độ dài của chuỗi, tức
length * 2 + 1
. Còn giá trị thì theo thông thường sẽ được lưu trữ tạikeccak256(S)
, vớiS
là vị trí của main slot.
Memory
Layout của memory
hoạt động dựa trên các slot 256 bit - 32 byte
Không gian riêng
0x00
và0x20
: Dành cho hàm băm0x40
và0x50
: con trỏ bộ nhớ trống0x60
: Bằng 0 vĩnh viễn và được sử dụng làm giá trị ban đầu cho các mạng bộ nhớ động trống
Giới hạn bộ nhớ tối đa
mStart.Uint64()
memory bắt đầu được ép về kiểu Uint64 điều đó chúng tỏ biến này không vượt quá được giá trị 2^64 - 1
, nếu vượt quá nó sẽ revert.
Con trỏ đến vị trí memory free
Có 5 byte 6080604052
đều xuất hiện trong mỗi smartcontract, chúng để làm gì vậy nhỉ?
Vì đẩy vào stack 80, đẩy vào stack 40, mstore sử dụng 2 tham số lần lượt lấy ra từ stack (40, 80) với (địa chỉ để ghi, giá trị cần ghi) và toàn bộ ý nghĩa của nó là: “T quy định chúng mày muốn thêm cái gì vào memory thì ghi từ địa chỉ 80, 00 → 70 bận rồi vì chúng nó có việc riêng cho hệ thống”