The HyVM — a VM (Virtual machine) on the EVM (Ethereum Virtual machine)

Vuong Tran
10 min readNov 26, 2022

--

TLDR: Nếu bạn chỉ quan tâm đến giải thích kỹ thuật về cách triển khai HyVM, chỉ cần chuyển sang phần “Đi sâu vào HyVM”.

🚀 Một số ngữ cảnh
Có rất nhiều giao thức DeFi tuyệt vời ngoài kia. Những người bản địa DeFi biết về chúng và sử dụng chúng hàng ngày. NHƯNG:

Tương tác với những thứ đó đòi hỏi bạn phải học cách chúng hoạt động.
Tương tác với nhiều giao thức (nếu không muốn nói là không thể) thường khó khăn trong một giao dịch.
Khi bạn đã thiết lập một chiến lược phức tạp, thường rất tẻ nhạt khi hoàn nguyên kỹ sư những gì bạn đã làm và loại bỏ nó theo cách thủ công.
Đối với những người không sử dụng DeFi, các chiến lược phức tạp nằm ngoài tầm với, ngay cả khi kế hoạch này được giải thích rõ ràng bởi người mà họ tin tưởng. Có quá nhiều thứ để học. Quá nhiều nền tảng cần hiểu để đủ tự tin.
Tại Nested, chúng tôi mong muốn cung cấp dịch vụ tổng hợp cho tất cả các giao thức DeFi và loại bỏ sự phức tạp đó.

Điều đó khiến chúng ta tự hỏi:

Làm cách nào để cung cấp các tính năng và tính linh hoạt tối đa mà không cần phải kiểm tra, triển khai và duy trì các hợp đồng thông minh đáng sợ mà về cơ bản phải làm *mọi thứ*?

Đó là lý do tại sao chúng tôi nghĩ ra HyVM

❓ HyVM là gì?
HyVM là một hợp đồng thông minh hoạt động như một máy ảo. Nó có thể tự động thực thi mã byte EVM mà không cần phải triển khai nó.

Tất nhiên, việc cho phép người dùng chạy mã byte tùy ý trên một hợp đồng mà nhiều người dùng chia sẻ ít nhất là không thể tưởng tượng được. Đó là lý do tại sao bạn sẽ cần một mô hình bảo mật dựa trên sự phân biệt người dùng (chẳng hạn như ngăn việc sử dụng cách tiếp cận như vậy đối với các giao thức yêu cầu tất cả tài sản của người dùng được lưu trữ trong một hợp đồng).

Nhưng khi trường hợp sử dụng của bạn cho phép cấu hình như vậy trong đó người dùng có hợp đồng riêng của họ, thì bạn sẽ có được sự linh hoạt tối đa với mức bảo trì trên chuỗi tối thiểu.

🔐 Mô hình bảo mật của Nested
Chúng tôi đã đạt được sự tách biệt tài sản của người dùng thông qua một phương pháp đơn giản:

Mỗi người dùng có bản sao HyVM được triển khai riêng (là proxy cho một triển khai duy nhất nhưng chúng ta đừng bận tâm)
Mỗi hợp đồng có một NFT liên quan
Các hợp đồng này chỉ cho phép thực hiện tùy ý khi chủ sở hữu của NFT được liên kết đã ký giao dịch.
Chúng tôi gọi những hợp đồng như vậy (+ NFT được liên kết của chúng) là “kho tiền”… chúng hoạt động giống như những chiếc ví do một người dùng sở hữu, có thể chuyển nhượng được (bằng cách chuyển giao NFT cơ bản).

📚 Một số điều kiện tiên quyết về EVM
EVM, hợp đồng, ngôn ngữ, mã byte
Nếu bạn đã học cách lập trình hợp đồng thông minh, bạn có thể đã làm điều đó bằng Solidity. Nhưng hợp đồng thông minh và Solidity KHÔNG giống nhau.

Bạn có thể đã gặp phải từ viết tắt man rợ “EVM” (viết tắt của Ethereum Virtual Machine)… đó là công cụ thực thi hợp đồng thông minh (ít nhất là trên các chuỗi khối tương thích với EVM).

Solidity chỉ là một ngôn ngữ (giống như nhiều ngôn ngữ khác như Vyper hoặc Huff để kể tên một số) được biên dịch thành “Bytecode”.

“Mã byte” đó được điều hành bởi EVM, không biết gì về Solidity, v.v.

Chúng ta đừng đi sâu vào cách triển khai hợp đồng (tức là constructorin Solidity, thực thi mã trong quá trình triển khai)… Nói một cách đơn giản, để chạy được, mã byte này cần được triển khai tại một địa chỉ tĩnh… bất kỳ ai gọi địa chỉ này sau này đều có thể để chạy nó.

Cách thực thi EVM hoạt động
nb: Nếu bạn biết các máy ảo khác hoạt động như thế nào, như CLR (VM đứng sau các ngôn ngữ như C# hoặc F#) hoặc JVM (đứng sau Java hoặc Kotlin), thì bạn sẽ cảm thấy như ở nhà: Mã byte EVM rất giống với MSIL hoặc JVM mã byte, mặc dù đơn giản hơn nhiều.

Mã byte là một luồng hướng dẫn, mỗi hướng dẫn được biểu thị bằng một byte (ngoại trừ push1…32 chiếm 2…33 byte).

Bạn có thể tìm thấy danh sách đầy đủ tất cả các hướng dẫn tại evm.codes

Mỗi hướng dẫn có một hành vi được xác định rõ ràng, có thể thực hiện một hoặc nhiều trong ba điều sau:

Sử dụng các giá trị từ “ngăn xếp”.
Đẩy một giá trị vào “ngăn xếp”.
Ảnh hưởng đến môi trường:
- đọc/ghi dữ liệu từ “bộ nhớ.”
- đọc/ghi dữ liệu từ “lưu trữ.”
- đọc dữ liệu từ dữ liệu cuộc gọi.
- gọi các hợp đồng khác.
- lấy dữ liệu từ các nguồn khác (Chúng tôi sẽ không đi vào chi tiết quá nhiều).
“ngăn xếp”, đúng như tên gọi của nó, chỉ là một chồng các giá trị 32 byte, là thứ bạn thao tác theo hướng dẫn. Bạn đưa ra hướng dẫn về những việc cần làm thông qua ngăn xếp này. Chẳng hạn, tính toán40+2 sẽ được thực hiện thông qua luồng hướng dẫn này:

push1 40 👉 will push the value "40" on stack
push1 2 👉 will push the value "2" on stack
add 👉 will pop two values from stack, and push the result of their addition on stack

“bộ nhớ” đúng như những gì bạn mong đợi: một không gian tuyến tính nơi bạn có thể lưu trữ những thứ sẽ được truy xuất sau này trong quá trình thực thi. Một số hướng dẫn cho phép ghi vào bộ nhớ.

“bộ lưu trữ” là một cặp khóa-giá trị nơi bạn có thể lưu trữ mọi thứ… nó tồn tại liên tục trong các lần thực thi.

Phần này không nhằm mục đích trở thành một hướng dẫn chính thức về những hiểu biết sâu sắc về EVM. Tôi sẽ để nó như vậy. Nó đủ để cung cấp cho bạn các gợi ý về cách thức hoạt động của nó hoặc liên quan đến các máy ảo khác mà bạn biết. Tôi khuyên bạn nên tìm hiểu sâu hơn về các tài nguyên chuyên dụng nếu bạn muốn biết thêm về nó.

Hãy đi sâu vào những thứ thực sự.

🔍 Tìm hiểu sâu về HyVM
👉 TLDR: Hợp đồng chính mà bạn muốn khám phá ở đây.

HyVM là một máy ảo, được viết bằng Huff.

Khi làm việc với VM, chúng ta thường gặp các khái niệm sau, sẽ được giải thích trong phần này.

Bản thân chương trình “máy chủ” chịu trách nhiệm chạy môi trường ảo hóa… ở đây, hợp đồng HyVM (chính nó chạy bên trong EVM)
Chương trình “khách”, ở đây là hợp đồng mà máy chủ chạy mà không thể nhận thấy rằng nó không chạy trong môi trường “bình thường” (nó sẽ không nhận thấy rằng đó không phải là hợp đồng đã triển khai)… đây, khách của chúng tôi chương trình được chuyển dưới dạng dữ liệu cuộc gọi tới HyVM.
phân biệt bộ nhớ máy chủ và khách: máy chủ và khách cần lưu trữ mọi thứ trong bộ nhớ, nhưng họ không thể biết về nhau.
Một từ về bộ nhớ:
Hợp đồng EVM cổ điển sẽ có thể phân bổ địa chỉ bộ nhớ từ 0 đến vô cùng. Có nghĩa là khi chạy với tư cách khách trong VM của chúng tôi, nó PHẢI có khả năng làm điều đó nếu chúng tôi muốn nó chạy như mong đợi.

Điều đó nói rằng, chủ nhà cũng cần lưu trữ những thứ trong bộ nhớ mà khách không thể truy cập được. Điều này đặt ra câu hỏi: Đặt bộ nhớ của máy chủ của chúng tôi ở đâu khi khách của chúng tôi cần có [0; +∞[ có thể truy cập?

Để giải quyết vấn đề này, chúng tôi sẽ phải biết máy chủ của chúng tôi cần bao nhiêu dung lượng bộ nhớ (đây phải là một số lượng cố định) và bù vào bộ nhớ của khách bằng dung lượng đó. Nói cách khác: Trên thực tế, địa chỉ 0x00 của khách sẽ được lưu trữ ở một địa chỉ cao hơn. Và sau đó, chúng tôi sẽ sửa các địa chỉ được đẩy tới các lệnh truy cập bộ nhớ theo số lượng đó (sẽ nói thêm về điều đó sau).

Sự thật thú vị: Các hệ điều hành (Linux, Windows,…) cách ly bộ nhớ kernel khỏi bộ nhớ người dùng bằng cách sử dụng cùng một loại kỹ thuật.

Phá vỡ cơ sở mã HyVM
Nó có một số phần đáng chú ý

MAIN() macro… điểm vào
CONTINUE() macro… macro “chạy opcode tiếp theo”
Tất cả các nhãn thực hiện lệnh (nhãn bắt đầu bằng op_)
FIX_MEMOFFSET() macro… chịu trách nhiệm sửa địa chỉ bộ nhớ của khách
Hãy đi sâu vào vấn đề này.

1️⃣ MAIN() chỉ làm hai việc:

Thiết lập bộ nhớ máy chủ đang tải cái gọi là “bảng nhảy”, là ánh xạ giữa các mã lệnh và nhãn triển khai của chúng
Bắt đầu thực hiện bằng cách gọi CONTINUE() đầu tiên
2️⃣ CONTINUE() chịu trách nhiệm chạy thao tác tiếp theo

Nó tải “con trỏ thực thi” từ bộ nhớ (tức là, con trỏ thực thi của chúng tôi = địa chỉ của các hướng dẫn sau sẽ được thực thi trong mã byte của chúng tôi)
Sau đó, nó di chuyển con trỏ thực thi này tới lệnh tiếp theo
Cuối cùng, nó chuyển sang “thực hiện lệnh” bên phải bằng cách sử dụng bảng nhảy được nạp bởi MAIN()
3️⃣ Hướng dẫn triển khai

Phần này là nơi mà việc thực hiện khách thực sự sẽ diễn ra.

Điều quan trọng cần hiểu là khi chuyển sang triển khai thực thi, máy chủ sẽ không để lại giá trị nào của riêng nó trên ngăn xếp => Tất cả các giá trị trên ngăn xếp sẽ được đẩy bởi hợp đồng khách.

Điều đó đảm bảo rằng không có giá trị dành riêng cho máy chủ lưu trữ nào được khách truy cập.

Việc triển khai hầu hết các hoạt động sẽ đơn giản như chỉ chạy lệnh EVM tương ứng rồi chuyển sang lệnh tiếp theo. Đó là lý do tại sao hầu hết các triển khai hoạt động trông đơn giản đến mức ngu ngốc và hầu như luôn giống nhau. Ví dụ, thêm:

op_add:
thêm 👈 chỉ cần thêm hai giá trị vào ngăn xếp
TIẾP TỤC() 👈 chuyển sang hướng dẫn tiếp theo
Điều đó nói rằng, như đã nêu trong phần “một từ về bộ nhớ”, tất cả các hướng dẫn đang đọc hoặc ghi vào bộ nhớ cần phải được cố định để giải thích cho bộ nhớ của máy chủ. Điều này được thực hiện bằng cách chạy FIX_MEMOFFSET() macro trên tất cả các giá trị ngăn xếp tham chiếu đến một con trỏ bộ nhớ.

Chẳng hạn, mload:

op_mload: 
FIX_MEMOFFSET() 👈 fixes the memory pointer on top of stack
mload 👈 executes the actual mload instruction on fixed address
CONTINUE() 👈 jump to next instruction

Tương tự như vậy, các hướng dẫn khác mang tính giai thoại hơn như PC hoặc bản sao mã sẽ được triển khai theo cách khác vì giá trị cơ bản của chúng được ảo hóa ( PC truy cập bộ đếm chương trình thực sự được lưu trữ trong bộ nhớ và bản sao mã sẽ phải tải mã từ dữ liệu cuộc gọi chứ không phải từ mã thực tế — đó là mã HyVM)

4️⃣ CỐ ĐỊNH_MEMOFFSET()

Khi bạn hiểu mọi thứ ở trên, việc thực hiện nó khá đơn giản, vì vậy tôi sẽ không đi sâu vào chi tiết.

🕷 Tóm tắt: Khai thác và giảm thiểu
Mỗi người dùng sở hữu hợp đồng của họ. Họ có thể làm bất cứ điều gì họ muốn với nó và thực thi bất kỳ đoạn mã nào họ muốn.

Hãy tưởng tượng trong một giây rằng tôi là một người dùng khét tiếng. Tôi biết tôi sẽ không bao giờ có thể truy cập tài sản của người dùng khác. NHƯNG… tôi có thể làm một cái gì đó như thế này:

Tạo một kho tiền
Triển khai một hợp đồng bất chính mà tôi sở hữu và có thể chuyểnFrom() một số tài sản
Chạy phê duyệt(usdc, nefariousContract, 0xffffffffffffffff) trên kho tiền của tôi
Chuyển quyền sở hữu của kho tiền này cho một số nạn nhân không nghi ngờ (sử dụng một số lý do mờ ám)
Đợi nạn nhân này thêm một số USDC vào kho tiền mới có được của anh ấy
Chạy hợp đồng bất chính của tôi để rút USDC của nạn nhân từ kho tiền cũ của tôi.
Điều này đòi hỏi nạn nhân phải ngây thơ một chút, điều này có thể dẫn đến các vấn đề không mong muốn.

Để giảm thiểu điều này, chúng tôi đã xây dựng một ngôn ngữ kịch bản tạo mã byte “an toàn”, có thể chạy trên HyVM. Chúng tôi sẽ không bao giờ để lại bất kỳ hiện vật nào (chẳng hạn như phê duyệt… nó sẽ tự động hủy phê duyệt mọi thứ theo thiết kế).

Nếu chúng tôi phát hiện thấy người dùng thực hiện giao dịch trên kho tiền của họ mà ngôn ngữ kịch bản của chúng tôi chưa tạo ra, thì kho tiền của họ sẽ bị loại khỏi nền tảng của chúng tôi.

Điều đó vẫn cho phép người dùng của chúng tôi tự do làm bất cứ điều gì họ muốn với tài sản của họ trong khi cung cấp tất cả các công cụ cho người dùng thông thường để biết khi nào kho tiền đã được chuyển cho họ không đáng tin cậy.

Tác giả: Olivier Guimbal.

Theo dõi và ủng hộ chúng tôi trên các phương tiện truyền thông

--

--

Vuong Tran

Crypto Researcher | Crypto project supporter