Sử dụng LLVM để tạo ngôn ngữ hợp đồng thông minh ETC của riêng bạn

Rất khó để tạo ra ngôn ngữ lập trình của riêng bạn. Mọi người phải có kiến thức về một trình biên dịch từ đầu đến cuối. Nhờ LLVM (là một framework trình biên dịch đa mục tiêu đa nguồn) giờ đây các nhà phát triển có thể tạo các ngôn ngữ của riêng họ mà không cần đi sâu vào chi tiết.

Bây giờ với dự án EVM-LLVM, LLVM có thể hỗ trợ tạo mã byte EVM, có nghĩa là chúng ta có thể bắt đầu tạo ngôn ngữ hợp đồng thông minh của riêng mình bằng LLVM.

Mục tiêu của bài viết này

Bài viết này cho thấy cách chúng ta có thể sử dụng EVM-LLVM để tạo ngôn ngữ đồ chơi Kaleidoscope để tạo ra các hợp đồng thông minh có thể triển khai chuỗi khối. Trong phiên bản bài viết này, chúng tôi sẽ không viết một trình biên dịch hoàn chỉnh bao gồm tất cả các lĩnh vực, nhưng chúng tôi sẽ đề cập đến các phần thiết yếu của việc chuyển ngôn ngữ đơn giản dựa trên LLVM sang nền tảng hợp đồng thông minh. Thông qua bài viết này, bạn sẽ có thể tìm ra cách mà bạn có thể tạo ngôn ngữ hợp đồng thông minh của riêng mình bằng cách sử dụng LLVM framework.

EVM là một kiến ​​trúc phần mềm rất đặc biệt, đòi hỏi trình biên dịch phải thực hiện một số công việc chuẩn bị trước khi chúng ta có thể triển khai nó lên blockchain. Trong phần tiếp theo, bạn sẽ thấy lý do tại sao chúng ta cần thực hiện một số công việc bổ sung để làm cho chương trình của chúng ta có thể chạy được trên blockchain.

Chuẩn bị

Tải xuống EVM-LLVM

Bạn cần gói phát triển EVM-LLVM để xây dựng ngôn ngữ hợp đồng thông minh của bạn.

git clone git@github.com:etclabscore/evm_llvm.git
cd evm_llvm
git checkout EVM
mkdir build && cd build
cmake -DLLVM_EXPERIMENTAL_TARGETS_TO_BUILD=EVM ..
make -j8

Vì EVM-LLVM là một backend khác trong dự án LLVM, mọi thứ đều tuân theo quy ước LLVM. Chúng ta cần các thư viện được biên dịch và các tiêu đề EVM-LLVM để tạo các hợp đồng thông minh của chúng ta.

Triển khai ngôn ngữ cơ bản

Hướng dẫn này bao gồm rất nhiều chủ đề, mà chúng ta không thể đề cập đầy đủ trong bài viết. Để tiết kiệm thời gian, chúng ta sẽ không chuyển sang ngôn ngữ hoàn chỉnh, nhưng chúng ta sẽ bắt đầu với các đoạn mã ví dụ ở cuối Chương 3. Bạn có thể tìm thấy những dòng mã bắt đầu tại:

https://github.com/etclabscore/evm_llvm/blob/master/examples/Kaleidoscope/Chapter3/toy.cpp

Việc triển khai Chương 3 ban đầu đã có một trình phân tích đơn giản, trình phân tích cú pháp và một bảng mã dựa trên AST. Bạn có thể viết một khai báo hàm sẽ hiển thị LLVM IR trên màn hình. Đó là những gì chúng ta cần chỉ ra các yếu tố cần thiết để xây dựng ngôn ngữ hợp đồng thông minh bằng LLVM. Tất nhiên, chúng ta có thể thêm nhiều tính năng hơn vào ngôn ngữ nhỏ của mình để làm cho nó tốt hơn, nhưng xin vui lòng tạm thời sử dụng nó, bởi vì chúng ta sẽ chỉ tập trung vào nội dung cơ bản.

Tóm lại, chúng ta muốn thực hiện các chức năng sau ở cuối bài viết này:

  • Biên dịch hàm Kaleidoscope hoàn chỉnh thành LLVM IR, sau đó sử dụng backend EVM để tạo mã byte EVM.
  • Tạo các chức năng hỗ trợ dành riêng cho EVM để đảm bảo rằng các hợp đồng thông minh nhỏ của chúng ta có thể tương tác với các tài nguyên đầu vào và đầu ra.

Thông số kỹ thuật ngắn gọn cho Kaleidoscopecó thể được tìm thấy trong các hướng dẫn của trang web LLVM. Kaleidoscope hoàn chỉnh bao gồm một trình biên dịch JIT, nhưng trong hướng dẫn này, chúng ta sẽ chỉ đề cập đến một tập hợp con của nó. Nói tóm lại, chúng ta muốn người dùng viết một khai báo hàm Kaleidoscope và trình biên dịch nhỏ của chúng ta có thể tạo EVM-LLVM IR có thể thực thi được.

Thay đổi việc triển khai trình biên dịch

Tiêu đề đặc trưng cho EVM (EVM-specific headers)

Để tạo một hợp đồng thông minh EVM đầy đủ chức năng, chúng ta cần truy cập vào một số ops cụ thể của EVM như CALLDATALOAD, RETURN. Sử dụng các hàm nội bộ để xuất các hướng dẫn cụ thể EVM cho các nhà phát triển trình biên dịch. Để truy cập các chức năng nội bộ đã xác định đó, chúng ta cần bao gồm các khai báo hàm nội tại EVM để các hàm nội tại được hiển thị:

#include "llvm/IR/IntrinsicsEVM.h"

Hỗ trợ số nguyên 256 bit

Các kiểu dữ liệu dấu phẩy động không được phép trên các chuỗi khối, bởi vì các máy khác nhau có thể có kết quả tính toán dấu phẩy động hơi khác nhau, điều này sẽ gây ra các hard fork trên chuỗi. Do đó, chúng ta cần chuyển đổi nó thành một kiểu số nguyên. Trong trường hợp này, tôi muốn giải thích trường hợp số nguyên 256 bit được hỗ trợ trong EVM-LLVM.

Trong LLVM, một số nguyên có độ dài tùy ý được lưu trữ bằng cách sử dụng lớpllvm::APInt. Để tạo số nguyên 256 bit, bạn chỉ cần gọi:

Value* int256 = ConstantInt::get(TheContext, APInt(256, "1234567890123456789", 10));

Chúng ta có thể gói nó bên trong một hàm để làm cho việc cụ thể hóa một số nguyên 256 bit dễ dàng hơn:

static Value* Get256ConstantInt(int64_t val) {
return ConstantInt::get(TheContext, APInt(256, val));
}

Trong ngôn ngữ nhỏ của chúng ta, chúng ta đã thay đổi cấu trúc NumberExprAST thành std::stringthay vì int64_t, vì vậy chúng tôi có thể xử lý các số nguyên 256 bit có kích thước gấp 4 lần int64_t:

class NumberExprAST : public ExprAST {
std::string Val;
public:
NumberExprAST(std::string Val) : Val(Val) {}
Value *codegen() override;
};Value *NumberExprAST::codegen() {
return ConstantInt::get(TheContext, APInt(256, Val, 10));
}

Lưu ý ở đây, chúng tôi sử dụng hàm tạo APInt kiểu chuỗi quá tải để phân tích chính xác các số nguyên 256 bit.

Thêm bộ điều phối chức năng hợp đồng thông minh

Bộ điều phối chức năng

Mọi hợp đồng EVM bắt đầu thực hiện từ đầu phần mã byte. Ban đầu, bộ nhớ và ngăn xếp là rỗng. Do đó, tùy thuộc vào trình biên dịch để tạo đoạn mã khởi tạo thích hợp để khởi động hệ thống. Vì vậy, các hợp đồng thông minh đòi hỏi một chức năng meta, mà ở đây chúng tôi gọi nó là “nhà phân phối chức năng”. EVM-LLVM có bố cục chức năng cụ thể để đảm bảo tạo hợp đồng thông minh chính xác.

Hàm khởi đầu là một hàm đặc biệt và được xử lý nó khác nhau bởi backend LLVM:

  • Nó phải được đặt tên là “main”
  • Kiểu trả về của nó phải là void
  • Nó không nên có liên kết. Đó là, không có hàm nào khác có thể gọi hàm khởi đầu.
  • Nó phải là chức năng đầu tiên trong Mô-đun IR LLVM
  • Nhà phát triển và người dùng không thể nhìn thấy nó

Tên đặc biệt “main” được đặt để xác định định nghĩa của hàm khởi đầu và trình biên dịch sẽ không tạo mã chương trình con trả về cho hàm khởi đầu (vì hàm khởi đầu luôn luôn noreturn- nó sẽ luôn kết thúc bằng lệnh RETURNhoặc REVERT). Trong trình biên dịch hợp đồng thông minh của chúng ta, chúng ta sử dụng một hàm để tạo hàm khởi đầu.

Function *GenerateWrapperFunction(Function* calleeF) {
FunctionType* FT = FunctionType::get(Type::getVoidTy(TheContext),
inputs, false);
Function *F = Function::Create(FT, Function::ExternalLinkage,
"main",
TheModule.get()); BasicBlock *BB = BasicBlock::Create(TheContext, "entry", F);
Builder.SetInsertPoint(BB);...
}

Lưu ý rằng GenerateWrapperFunction sẽ có đầu vào là hàm callee (hàm “wrapped”) làm tham số. Hàm callee là hàm chúng ta thực sự muốn thực thi. Trong ví dụ này, chúng ta muốn thực hiện chức năng callee chỉ vì chúng ta cần tạo mã để trích xuất các đối số hợp đồng thông minh.

Khởi tạo frame pointer

Có nhiều cách chúng ta có thể thực hiện hợp đồng thông minh. Các kiến ​​trúc hiện đại sử dụng cơ chế chuyển đổi ngữ cảnh hỗ trợ phần cứng để gọi chương trình con. Các hướng dẫn như JUMP AND LINKsẽ lưu trữ bối cảnh hiện tại vào một thanh ghi đặc biệt trước khi bắt đầu thực hiện chương trình con.

Các hợp đồng EVM do LLVM duy trì biên dịch một frame pointer trong bộ nhớ để ghi lại địa chỉ bắt đầu của call frame (cũng được lưu trong bộ nhớ). Xem trang wiki này để biết thêm chi tiết. Lý tưởng nhất là các mã khởi tạo phụ thuộc vào máy không nên xuất hiện trong LLVM IR.

Trong trường hợp EVM, trình biên dịch không thể làm điều đó cho các nhà phát triển hợp đồng, bởi vì bạn có thể có nhiều cách để thực hiện bộ điều phối chức năng của mình. Một ngôn ngữ frontend có thể có ý tưởng riêng của họ về điều phối chức năng. Vì vậy, chúng tôi nghĩ rằng việc tiết lộ các chi tiết của bộ điều phối chức năng cho một ngôn ngữ frontend sẽ cho phép tự do triển khai một hợp đồng thông minh mới.

Trong triển khai này, trình biên dịch vi mô của chúng ta sẽ kiểm soát hoàn toàn và chịu trách nhiệm khởi tạo. May mắn thay, chúng ta chỉ có một hướng dẫn IR để khởi tạo bối cảnh.

Frame pointer được lưu trữ tại địa chỉ bộ nhớ 0x40. Bạn nên khởi tạo nó thành một giá trị lớn hơn hoặc bằng 0x60(để phần ngăn xếp bộ nhớ không bị chồng chéo với chính frame pointer). Đồng thời, giá trị phải được liên kết thành 32 byte. Trong ví dụ của chúng ta, chúng ta khởi tạo giá trị thành 128.

EVM-LLVM đã có sẵn opcode EVM MSTOREnhư một hàm nội tại để sửa đổi rõ ràng một địa chỉ bộ nhớ EVM, đặc biệt được sử dụng để khởi tạo frame pointer. Khi tiêu đề nội bộ EVM cụ thể, chúng ta có thể tạo một Intrinsic::evm_mstoređể thực hiện công việc bao gồm:

Value* addr = Get256ConstantInt(64);
Value* val = Get256ConstantInt(128);
Function *mstore = Intrinsic::getDeclaration(
TheModule.get(), llvm::Intrinsic::evm_mstore, llvm::None);
Builder.CreateCall(mstore, {addr, val});

Trích xuất các giá trị đầu vào EVM

Không giống như các nền tảng khác, EVM sử dụng CALLDATALOADopcode để trích xuất các đầu vào đối số blockchain rõ ràng. Điều này sẽ tạo ra một loạt các CALLDATALOADhướng dẫn để trích xuất các đối số đầu vào:

std::vector<Value*> extractedParams;
for (size_t i = 0; i < calleeTy->getNumParams(); ++i) {
Value* val_int = Get256ConstantInt(i * 32);
Function *calldataloadF = Intrinsic::getDeclaration(
TheModule.get(), llvm::Intrinsic::evm_calldataload, llvm::None);
CallInst * calldataload = Builder.CreateCall(calldataloadF, {val_int});
extractedParams.push_back(calldataload);
}
CallInst *call_calleeF = Builder.CreateCall(calleeF, extractedParams);

Trả về giá trị từ EVM

Hàm khởi đầu có thể phải trả về giá trị từ hợp đồng thông minh, trong trường hợp hàm được xác định cần trả về giá trị cho người dùng. EVM sử dụng một phương pháp đặc biệt để trả về giá trị từ hợp đồng thông minh. Đầu tiên, giá trị trả về phải được lưu trong địa chỉ bộ nhớ. Sau đó, gọi RETURNopcode EVM và chỉ định điểm bắt đầu và kết thúc của vùng nhớ.

Value* addr_int = Get256ConstantInt(0);
Function *mstore = Intrinsic::getDeclaration(
TheModule.get(),
llvm::Intrinsic::evm_mstore,
llvm::None);
Builder.CreateCall(mstore, {addr_int, call_calleeF});Function *evm_return = Intrinsic::getDeclaration(
TheModule.get(),
llvm::Intrinsic::evm_return,
llvm::None);
Builder.CreateCall(evm_return,
{Get256ConstantInt(0), Get256ConstantInt(32)});

Di chuyển hàm khởi đầu vào đầu danh sách hàm

EVM-LLVM tạo các hàm theo thứ tự của các hàm được xác định. Vì vậy, chúng ta nên đảm bảo hàm khởi đầu của chúng ta, hàm mainxuất hiện như là hàm đầu tiên trong danh sách:

auto *wrapper = GenerateWrapperFunction(FnIR);// You should include "llvm/IR/SymbolTableListTraits.h" here
using FunctionListType = SymbolTableList<Function>;
FunctionListType &FuncList = TheModule->getFunctionList();
FuncList.remove(wrapper);
FuncList.insert(FuncList.begin(), wrapper);

Biên dịch và liên kết ngôn ngữ frontend của chúng ta

llvm-configcó thể được sử dụng để chỉ định các cờ và thư viện liên kết khi tích hợp backend LLVM. Trong trường hợp của chúng ta, chúng ta phải sử dụng EVM-LLVM llvm-configđể lấy đúng đường dẫn. Dưới đây là một ví dụ gọi llvm-configđể đưa thêm vào các tùy chọn biên dịch.

clang++ -g toy.cpp `~/workspace/evm_llvm/build/bin/llvm-config --cxxflags --ldflags --system-libs --libs core` -o evmtoy

Tạo LLVM IR

Trình biên dịch nhỏ của chúng ta có thể tạo LLVM IR cụ thể, sẽ được sử dụng để tạo mã byte EVM. Dưới đây là một ví dụ về đầu phát ra:

ready> def foo(a b) a*b + 2 * a + 3 * b;
ready> Emitting Smart contract IR:; ModuleID = 'My cool EVM function'
source_filename = "My cool EVM function"define void @main() {
entry:
call void @llvm.evm.mstore(i256 64, i256 128)
%0 = call i256 @llvm.evm.calldataload(i256 0)
%1 = call i256 @llvm.evm.calldataload(i256 32)
%2 = call i256 @foo(i256 %0, i256 %1)
call void @llvm.evm.mstore(i256 0, i256 %2)
call void @llvm.evm.return(i256 0, i256 32)
unreachable
}define i256 @foo(i256 %a, i256 %b) {
entry:
%multmp = mul i256 %a, %b
%multmp1 = mul i256 2, %a
%addtmp = add i256 %multmp, %multmp1
%multmp2 = mul i256 3, %b
%addtmp3 = add i256 %addtmp, %multmp2
ret i256 %addtmp3
}; Function Attrs: nounwind writeonly
declare void @llvm.evm.mstore(i256, i256) #0; Function Attrs: nounwind readnone
declare i256 @llvm.evm.calldataload(i256) #1; Function Attrs: noreturn nounwind
declare void @llvm.evm.return(i256, i256) #2attributes #0 = { nounwind writeonly }
attributes #1 = { nounwind readnone }
attributes #2 = { noreturn nounwind }

Chạy trình biên dịch nhỏ của chúng ta

Tạo mã byte EVM

Hãy sao chép và dán LLVM IR được tạo vào một tệp và kích hoạt EVM-LLVM của chúng tallcđể có được EVM assembly:

build/bin/llc -print-after-all -debug -mtriple=evm -filetype=asm toy.ll

Nó sẽ tạo ra một tệp toy.s chứa tập hợp EVM được tạo để bạn kiểm tra. Để tạo tệp nhị phân EVM, hãy thêm -filetype=obj vào tùy chọn:

build/bin/llc -print-after-all -debug -mtriple=evm -filetype=obj toy.ll

Bây giờ chúng ta có toy.stoy.o. Nhưng để thực hiện nó, chúng ta chỉ cần tệp mục tiêu. Một bước cuối cùng: để chạy nó trong dòng lệnh, chúng ta phải trích xuất nhị phân thành một chuỗi hex. Tôi sử dụng một tập lệnh Python đơn giản để thực hiện công việc:

#!env python3
import sysdef get_contract(inputfile: str) -> str:
import binascii
output = []
with open(inputfile, 'rb') as file:
byte = file.read()
output.append(binascii.hexlify(byte).decode("utf-8"))
output_str = ''.join(output)
print(output_str)if __name__ == "__main__":
get_contract(sys.argv[1])

Chỉ cần xác định toy.olàm đối số cho tập lệnh để nhận tệp nhị phân EVM bằng chuỗi thuần túy:

5b6080604052602080356000808035610040909192939091604051806108200152604051610840016040526004580192565b60405160209003516040529052f35b8082029190910290019056

Chạy nó nào!

Bây giờ là lúc để thực hiện hợp đồng thông minh EVM mới được tạo của chúng ta! Hãy kích hoạt EVMcông cụ của Geth trong dòng lệnh của chúng ta và nó sẽ chạy lệnh cục bộ.

Các tùy chọn dòng lệnh giống như sau:

evm --input 0000000000000000000000000000000000000000000123456789001234567890000000000000000000000000000000000000000000098765432109876543210 --code 5b6080604052602080356000808035610040909192939091604051806108200152604051610840016040526004580192565b60405160209003516040529052f35b8082029190910290019056 run

Đây là cách chúng tôi phân tích nó. Đối số --input chỉ định giá trị đầu vào cho hợp đồng thông minh và --codechỉ định mã byte hợp đồng thông minh. Các --inputtùy chọn là một chuỗi các mã hex chứa đầu vào cho các hợp đồng thông minh. Thông thường nó chứa chữ ký hàm và các đối số cho hàm.

Trong hàm đầu vào nhỏ được triển khai ở trên, chúng ta không yêu cầu người dùng chỉ định thông tin ngoài hai số nguyên 256 bit. Chúng ta chắc chắn có thể làm nhiều việc hơn, chẳng hạn như thêm chữ ký hàm trong trường đầu vào và cơ chế chọn chức năng trong hàm khởi đầu hoặc phân tích các loại tham số khác nhau.

Ngoại trừ CALLDATALOADtrường đầu vào này không hiển thị với EVM và hướng dẫn này được sử dụng cụ thể để trích xuất các giá trị từ trường đầu vào.

Kết quả phân tích

Sau khi EVM kết thúc thực thi, evm gửi trả kết quả về từ hợp đồng thông minh:

0x00000674f561a2226dd39084c7dccd2395a994d8e52577362432026874293089

Kết quả này là 32 byte chúng ta đã sao chép bằng cách sử dụng RETURNhướng dẫn ở cuối hàm khởi đầu của chúng ta.

Các chủ đề codegen LLVM IR khác

  • alloca lệnh phân bổ một đối tượng khung 32 byte (đối tượng hàm cục bộ) trên khung bộ nhớ của hàm hiện tại.
  • Không gian bộ nhớ được lập chỉ mục.
  • Nói chung, thông tin ABI của hợp đồng thông minh EBI được gửi bởi ngôn ngữ frontends. Nhưng tất nhiên chúng ta có thể viết một LLVM IR để phát ra hợp đồng ABI.

Những hạn chế

EVM được thiết kế để thực hiện công việc xác định. Bởi vì điều này, một số tiện ích chúng ta đang sử dụng hàng ngày không thể truy cập được. Đây là danh sách những điều này:

  • Không có không gian heap. Bạn không thể gọi llvm.mallochoặc một cái gì đó tương tự để có được cấp phát một không gian bộ nhớ động. Nếu bạn thực sự cần một không gian heap, bạn có thể viết một hàm malloc để cấp phát động không gian bộ nhớ.
  • Không hỗ trợ dấu phẩy động. Rõ ràng, EVM không hỗ trợ bất kỳ dấu phẩy động nào vì điều đó rất có thể sẽ phá vỡ sự đồng thuận. Giải pháp thay thế là sử dụng phần mềm thư viện mô phỏng được triển khai để tính toán dấu phẩy động nếu cần.
  • Không nhảy động (dynamic jumps). Nhảy động làm cho phân tích tĩnh và xác minh khó khăn. EVM có một no-op cụ thể, hướng dẫn giữ chỗ JUMPDESTphục vụ cho việc dán nhãn của điểm nhảy đến. Việc nhảy đến một địa chỉ non-JUMPDEST sẽ chấm dứt thực thi EVM.

Vì vậy, EVM là một môi trường thực thi cụ thể theo miền, bị hạn chế, không thể mong đợi nó thực thi tất cả các chương trình hiện có.

Điều gì tiếp theo?

Rất nhiều việc để tạo (porting) một ngôn ngữ hợp đồng thông minh nhỏ! Đây không phải là cách thực hiện ngôn ngữ hoàn chỉnh của người Viking, nhưng nó cho thấy các thành phần thiết yếu chúng ta cần để tạo hợp đồng thông minh EVM bằng EVM-LLVM. Với cơ sở hạ tầng LLVM, chúng ta chắc chắn có thể tạo ra một ngôn ngữ phức tạp và hữu ích hơn nhiều có lợi cho thế giới blockchain.

Chắc chắn có rất nhiều thứ chúng ta có thể làm để làm cho trình biên dịch nhỏ bé của chúng ta tốt hơn. Đây là một số ý tưởng:

  • Tạo một pipeline hoàn chỉnh để gửi nguồn hợp đồng thông minh của bạn tới mã nhị phân EVM.
  • Thêm các cơ chế phức tạp hơn vào ngôn ngữ của bạn. Chẳng hạn như: triển khai các chức năng khác của Kaleidoscope, các hoạt động cụ thể của EVM hoặc triển khai lên một blockchain hỗ trợ Ethereum Classic!!
  • Tạo triển khai mã thông báo ERC20 bằng ngôn ngữ hoàn toàn mới của bạn.

Nguồn: https://medium.com/etc-core/creating-your-own-smart-contract-languages-using-llvm-a83a9aa1e0c1

— — — — — — — — — — — — — — — — — — — — — -
Tham gia thảo luận và cập nhật tin tức mới nhất trên các kênh chính thức của chúng tôi:
- Facebook: facebook.com/EthereumClassicVietnam/
- Telegram: t.me/ETCVietnam
- Twitter: twitter.com/etcvietnam
- Medium: medium.com/@ETCVietnam

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Cộng Đồng Ethereum Classic Việt Nam
Cộng Đồng Ethereum Classic Việt Nam

Written by Cộng Đồng Ethereum Classic Việt Nam

Chào mừng bạn đến với channel của cộng động EthereumClassic Việt Nam! 😊 etclabs.org

No responses yet

Write a response