
Over-Engineering trong Layer2: Khi code phức tạp trở thành gánh nặng
Vũ Dũng
Một dòng log bất thường trong contract của một rollup đang hot gần đây khiến tôi phải dừng lại. Gas sử dụng cho một lần xác thực block cao gấp 3 lần dự kiến. Tôi mở mã nguồn và thấy một chuỗi các contract phụ, mỗi contract lại gọi qua một proxy khác, xen kẽ các hàm verify chồng chéo. Không có lỗi logic, nhưng hiệu suất thì tệ. Tôi nhớ lại một câu nói của một đồng nghiệp cũ: “Một lỗi gas nhỏ có thể làm sập cả một hệ thống.” Ở đây, không có lỗi, chỉ có sự dư thừa do quá cẩn thận.
Bối cảnh Layer2 đang chứng kiến một cuộc chạy đua về tính năng: proofs siêu nhanh, DA linh hoạt, sequencing phi tập trung. Các đội phát triển thường áp dụng kiến trúc modular, tách từng thành phần thành contract riêng để dễ nâng cấp. Điều này tạo ra một lớp trừu tượng dày đặc. Nhưng có một nghịch lý: càng nhiều lớp abstraction, càng nhiều chi phí gas cho việc truyền dữ liệu qua các contract. Trong một thị trường tăng, ai cũng muốn ra mắt nhanh. Các team thường copy-paste kiến trúc từ những dự án thành công mà không tối ưu cho use case cụ thể của mình. Kết quả là một mớ code “chạy được” nhưng phình to không cần thiết.
Tôi phân tích chi tiết contract của dự án đó. Họ triển khai một hệ thống Fraud Proof theo chuẩn Arbitrum nhưng thêm vào một lớp Merkle tree riêng để xác thực state root. Mỗi block, họ phải gọi 3 contract: đầu tiên là SequencerInbox, sau đó là RollupCore, cuối cùng là ChallengeManager. Mỗi bước đều có một loạt internal call, push state, emit event. Kết quả: một block đơn giản tiêu tốn 1.2 triệu gas – gấp 4 lần so với con số lý thuyết 300k gas. Tôi đào sâu và thấy thủ phạm: một hàm verifyWithProof trong ChallengeManager đã không tận dụng cached data từ contract cha, mà mỗi lần gọi lại đọc lại toàn bộ storage từ đầu. Một lỗi thiết kế tưởng chừng nhỏ, nhưng cộng dồn qua hàng nghìn block. Điều tôi phát hiện ra là không chỉ có một vấn đề: có tới 7 chỗ tối ưu có thể giảm 40% gas, và tất cả đều đến từ việc “over-engineering” – họ xây dựng quá nhiều mà không đo lường.
Quan điểm của tôi ở đây mang tính phản trực giác: việc modular hóa quá mức trong Layer2 đang trở thành một “regression” cho hiệu suất thay vì là một lợi thế. Các đội phát triển bị ám ảnh bởi tính linh hoạt và khả năng nâng cấp, nhưng lại quên mất rằng mỗi lớp trừu tượng là một lần tăng gas. Tôi từng chứng kiến một dự án Optimistic Rollup đơn giản chỉ với 2 contract chính mà đạt TPS cao hơn 30% so với một dự án cùng loại có 8 contract. Sự khác biệt nằm ở chỗ họ dám viết code monolithic cho những phần lõi, và chỉ modular hóa khi thực sự cần. Thị trường hiện tại đang tạo ra một vòng luẩn quẩn: các team nhỏ muốn gây ấn tượng với nhà đầu tư bằng kiến trúc “hiện đại” mà không hiểu rõ trade-off. Nếu bạn audit code, bạn sẽ thấy những điểm mù bảo mật không đến từ lỗi logic, mà từ sự phức tạp thừa thãi tạo ra bề mặt tấn công mới.
Kết luận của tôi sau 15 năm quan sát ngành: trong Layer2, “less is more” vẫn là chân lý. Đừng vội tin vào những slide kiến trúc 20 layer. Hãy mở code, đếm số lần delegatecall, đo gas từng hàm. Nếu team của bạn đang viết contract thứ 10 cho một tính năng có thể gộp vào 2 contract, hãy dừng lại. Câu hỏi tôi để lại cho bạn: bạn có đang over-engineer cho hệ thống của mình không? Hay bạn đang tạo ra một gánh nặng gas cho người dùng?