LapTrinhBlockchain

Chia sẻ kiến thức về Lập Trình Blockchain

Kiến thức Blockchain, Kiến thức phần mềm, Nâng cao Kiến thức

Hướng dẫn chạy fullnode trên TRON sử dụng EC2 của Amazon

Hướng dẫn chạy fullnode trên TRON sử dụng EC2 của Amazon

Hướng dẫn chạy fullnode trên TRON sử dụng EC2 của Amazon

Chia sẻ bài viết
5
(8)

Mạng lưới blockchain của TRON hiện tại có 2 node chính:

Ngoài ra còn có rất nhiều fullnode khác, bạn có thể truy vấn dựa vào api tronWeb.trx.listNodes. Có giao diện hiển thị các node và vị trí của nó: Nodes Distribution. Danh sách các Nodes và vị trí có thể lấy qua API: Nodemap

Nhưng khi sử dụng public node này thì bạn bị giới hạn số lượng request và thời gian phản hồi cũng chậm hơn. Cung do nhu cầu nghiệp vụ nên tôi cần triển khai Fullnode trên nền tảng blockchain của TRON. Bạn viết này hướng dẫn các bạn chạy Fullnode trên TRON, đồng thời đưa ra cách xử lý một số vấn đề gặp phải khi cài đặt.

Có thể bạn quan tâm: Hướng dẫn lập trình và triển khai Smart Contract trên TRON network

Lựa chọn cấu hình

Theo cấu hình đội dự án recommend trên Deploy A Node thì cần CPU 16 Cores + 32G RAM thì tôi thấy con a1.4xlarge (16 vCPUs + 32G RAM + ARM Architect => 0.47$/giờ) cấu hình phù hợp cấu hình mà có chi phí rẻ nhất, nhưng bạn phải chọn kiến trúc ARM mới chọn được loại này. Nhưng khi sử dụng kiến trúc ARM thì lại không chạy được tron node, khi chạy báo lỗi: “Can’t load AMD 64-bit .so on a AARCH64-bit platform“. Thực tế java không phụ thuộc vào OS và kiến trúc, nhưng lại phụ thuộc vào các thư viện mà ứng dụng sử dụng (Tệp *.so).

Sau đó tôi phải chuyển qua cấu hình t3.2xlarge. Cấu hình chi tiết như sau:

  • t3.2xlarge: 8 vCPUs + 32G RAM + x86 Architect (0.4224$/giờ)
  • OS: Ubuntu 22
  • SSD: GP3 2600G (0.1$/1G/1 tháng)
  • Chi phí: 564$

Cài đặt Fullnode trên Mainnet

Đầu tiên chúng ta cần cài Oracle JDK 1.8 bằng lệnh sau:

# Cài đặt
sudo apt-get update
sudo apt-get install openjdk-8-jdk

# Kiểm tra
java -version

Chúng ta tạo thư mục tron-node, tất cả dữ liệu liên quan ta sẽ đặt trong thư mục này:

mkdir tron-node
cd tron-node

Tiếp theo chúng ta lấy bản snapshot mới nhất từ Main Net Database Snapshots, lấy từ Trongrid Data Source thì có bản mới nhất. Để giảm thiểu dung lượng SSD chúng ta thực hiện song song vừa tải về vừa giải nén bằng lệnh sau:

# Vừa tải dữ liệu về đồng thời giản nén
# Với dữ liệu 1.85T thì mất khoảng 10h
# Mặc định dữ liệu tải về được đẩy vào thư mục output-directory
wget -qO- https://fullnode-backup-3.s3-ap-southeast-1.amazonaws.com/FullNode-56813712-4.7.3-output-directory.tgz | tar xvz

# Chuyển sang thư mục data
mv output-directory data

Với kích thước khoảng 1.85T thì tổng thời gian đâu đó khoảng 10h.

Bây giờ chúng ta tải tệp FullNode.jar từ địa chỉ the released jar.

mkdir fullnode
cd fullnode
wget https://github.com/tronprotocol/java-tron/releases/download/GreatVoyage-v4.7.3/FullNode.jar

Tiếp theo tạo tệp main_net_config.conf, copy nội dung từ main_net_config.conf. Sau đó sửa tệp này để cập nhật một số cấu hình:

vm.supportConstant = true

Tiếp theo chúng ta tạo tệp start-node.sh với nội dung như sau:

#!/bin/bash

java -Xmx24g -XX:+UseConcMarkSweepGC -jar FullNode.jar -c main_net_config.conf -d /home/ubuntu/tron-node/data

Sau đó chạy bằng lệnh sau:

chmod +x start-node.sh
./start-node.sh

Bạn xem log bằng lệnh sau:

# Xem log realtime mới nhất
tail -f logs/tron.log

# Xem block đang được đồng bộ
tail -f logs/tron.log | grep "Block num"

Sau khi đã đồng bộ thì block sẽ là block mới nhất khớp với trên TronScan:

Node đồng bộ: Block mới nhất khớp với trên TronScan

Sau khi Fullnode chạy OKIE, thông tin các API để kết nối tới Fullnode như sau:

Fullnode: http://localhost:8090
SolidityNode: http://localhost:8091
Event: tcp://127.0.0.1:5555

Một số lệnh test rpc theo mô tả trong FullNode HTTP API:

# Lấy thông tin block hiện tại
curl --request POST --url http://localhost:8090/wallet/getnowblock --header 'accept: application/json'

# Lấy thông tin của Node
curl --request GET --url http://localhost:8090/wallet/getnodeinfo --header 'accept: application/json'

# Lấy thông tin của một account
curl --request POST --url http://localhost:8090/wallet/getaccount --header 'accept: application/json' --header 'content-type: application/json' --data '{"address": "TZ4UXDV5ZhNW7fb2AMSbgfAEZ7hWsnYS2g","visible": true}'

Triển khai Dev Node chạy trên Local để phục vụ phát triển

Thực tế, trên TRON nếu bạn test trực tiếp trên Mainnet thì rất tốn tiền, nếu bạn test trên Testnet (Nile hoặc Shasta) thì cũng cần rất nhiều TRX testnet. Do đó khi phát triển, việc dựng 1 Dev Node chạy trên Local để phục vụ phát triển là điều hết sức cần thiết. Bạn có thể tham khảo TronBox Quickstart để biết cách làm.

Để chạy 1 Dev Node chúng ta chỉ cần chạy lệnh sau:

# 1. Cài đặt docker, chỉ làm lần đầu tiên
sudo apt update && sudo apt upgrade -y
curl -fsSL https://get.docker.com -o get-docker.sh
sh get-docker.sh
rm get-docker.sh

# 2. Tải image về, chỉ làm 1 lần đầu tiên
docker pull tronbox/tre

# 3. Chạy Dev Node
docker run -it -p 9090:9090 --rm --name tron tronbox/tre

Bạn nhìn màn hình console hiển thị danh sách các “Available Accounts” và “Private Keys” là okie nhé.

Các thông số để kết nối Dev Node:

Fullnode: http://127.0.0.1:9090
SolidityNode: http://localhost:8091
EventNode: tcp://127.0.0.1:5555

Nếu bạn muốn kết nối từ máy khác tới thì bạn thay 127.0.0.1 bằng Public IP và nhớ mở kết nối.

Liệu có thể sử dụng HardHat để fork cho TRON Mainnet?

Do đã làm nhiều trên Môi trường dạng EVM, nên tôi hay sử dụng HardHat để fork mainnet ở một block nào đó. Nhờ việc này mà ta có thể dễ dàng kiểm tra dữ liệu và test lại giao dịch ở các block trước đó. Và tôi tự hỏi rằng, với Tron là một Tvm, thì liệu có thể fork được không?

Bình thường, RPC trên TRON theo chuẩn “FULL NODE HTTP API” và “FULL NODE SOLIDITY HTTP API” (https://api.trongrid.io), API này hoàn toàn khác với JSON API trên các EVM. Nhưng đọc thêm tài liệu trên TRON, tôi thấy TRON cũng hỗ trợ JSON API xem FULL NODE JSON-RPC API và tôi thấy các API của nó khá giống với trên EVM. Đến đây tôi thấy có hy vọng sử dụng HardHat để fork cho TRON.

Tôi đã tìm thấy FULL NODE JSON-RPC API: https://nile.trongrid.io/jsonrpc. Tôi test thử với hàm eth_call, cái đầu tiên thì okie thấy có hy vong, nhưng 2 cái sau thì báo lỗi:

# Test Case 01: Truy vấn ở block mới nhất => OKIE
# Output: {"jsonrpc":"2.0","id":1,"result":"0x00000000000000000000000000000000000000000000000000000000036d6046"}
curl -X POST 'https://api.trongrid.io/jsonrpc' --data '{ "jsonrpc": "2.0", "method": "eth_call", "params": [{"from": "0xF0CC5A2A84CD0F68ED1667070934542D673ACBD8", "to": "0x32a4f47a74a6810bd0bf861cabab99656a75de9e", "data": "0x42cbb15c" }, "latest"], "id": 1}'

# Test Case 02: Truy vấn ở block pending => NOT OKIE
# Output: {"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"TAG [earliest | pending] not supported","data":"{}"}}
curl -X POST 'https://api.trongrid.io/jsonrpc' --data '{ "jsonrpc": "2.0", "method": "eth_call", "params": [{"from": "0xF0CC5A2A84CD0F68ED1667070934542D673ACBD8", "to": "0x32a4f47a74a6810bd0bf861cabab99656a75de9e", "data": "0x42cbb15c" }, "pending"], "id": 1}' 

# Test Case 03: Truy vấn ở block bất kỳ
# Output: {"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"QUANTITY not supported, just support TAG as latest","data":"{}"}}
curl -X POST 'https://api.trongrid.io/jsonrpc' --data '{ "jsonrpc": "2.0", "method": "eth_call", "params": [{"from": "0xF0CC5A2A84CD0F68ED1667070934542D673ACBD8", "to": "0x32a4f47a74a6810bd0bf861cabab99656a75de9e", "data": "0x42cbb15c" }, "57475868"], "id": 1}'

Kết quả trả về này rất giống với mô tả eth_call trong tài liệu của TRON. Việc không hỗ trợ theo blockNumber thì không thể thực hiện fork ở block bất kỳ. Nhưng vẫn còn cơ hội thực hiện fork ở block mới nhất..

Tôi tiếp chạy thử hardhat và vẫn báo lỗi:

npx hardhat node --fork https://api.trongrid.io/jsonrpc
-----------------------------
Error HH604: Error running JSON-RPC server: the method eth_getTransactionCount does not exist/is not available

Thực sự trên TRON không có khái niệm NONCE vì thế không cần api eth_getTransactionCount thật.

Như vậy TRON hỗ trợ API không đầy đủ, nên không thể hardfork được.

Tìm hiểu về Super Representatives (SR – Siêu đại diện)

Trên mạng TRON, tất cả chủ sở hữu TRX đều có thể đăng ký trở thành SR Candidates (Ứng cử viên SR) và có cơ hội trở thành SRs hoặc đối tác SR Partners.

  • Bất kỳ chủ sở hữu TRX nào cũng có thể bỏ phiếu cho các SR Candidates, trong đó 27 ứng cử viên được bình chọn nhiều nhất sẽ trở thành SRs, còn ứng cử viên thứ 28 đến 127 sẽ trở thành SR Partners. Từ thứ 128 trở đi đều là SR Candidates.
  • SRs chịu trách nhiệm tạo khối và đóng gói các giao dịch để họ nhận được phần thưởng biểu quyết và phần thưởng khối. Trong khi đó, các SR Partners chỉ nhận được phần thưởng biểu quyết.
  • Tất cả các SR Candidates, SR PartnersSRs đều có quyền đưa ra các đề xuất sửa đổi các tham số trên mạng TRON.

Thông tin các SRs bạn có thể xem tại: Super Representatives. Qua kiểm tra cấu hình một số SRs thì thực sự quá khủng, nhưng đây chỉ là mô tả của họ, không rõ thực tế thế nào?

  • CallmeSR:
    • Cấu hình: R4.16 xlarge 64 core – 488 GB RAM – 1T SSD storage (EBS)
    • Vị trí: Beijing, China
  • TronSpark:
    • Cấu hình:
      • Google Compute Instance: 64 CPU cores – 416 GB RAM – 1T SSD storage (EBS) – 25Gbs Bandwidth
      • Operating System: Ubuntu 16.04 LTS
    • Vị trí: United States East Region (South Carolina)
    • Chi phí: $180,000.00 USD / Năm
  • TRONScan:
    • Cấu hình: Intel Xeon processor E5-2670, 3.3 Ghz, 32 cores – 192 GB RAM – 3TB SSD (100.000 iops) – 40 Gbps Bandwidth
    • Vị trí: United States,San Francisco
    • Chi phí: $20K / Năm
  • CryptoChain:
    • São Paulo – Brazil (Primary Mainnet node)
      • Cloud AWS x1.32 xlarge
      • CPU: 128 cores
      • RAM: 1952GB
      • SSD: 2 x1952GB
      • Network: 25Gbps
    • Fortaleza – Brazil (Testenet/Backup Node)
      • Bare metal server
      • CPU: 24 cores
      • RAM: 128 GB
      • SSD: 500GB RAID 10
      • Network: 1Gbps
    • Virginia – US (Testenet /Mainnet Redundancy/Backup Node)
      • Bare metal server
      • CPU: 72 cores
      • RAM: 728 GB
      • SSD: 2TB GlusterFS
      • Network: 10Gbps
  • TRONLink:
    • Cấu hình:
      • Bare-metal server
      • CPU: Intel Xeon Processor E5-2670, 3.3 Ghz
      • Core Processor: 32-Core
      • RAM: 192 GB
      • SSD: 3 TB (100.000 IOPS)
      • Network: 40 Gbps
    • Vị trí: Beijing, China
  • CryptoGuyInZA:
    • Cấu hình:
      • Testnet node: – m5.xlarge
      • Mainnet nodes:
        • 2 hoặc 3 nodes.
        • x1.16 xlarge 64 core – 1T memory – 20T SSD storage (EBS) – 25G bandwidth
    • Vị trí: AWS United States East (Ohio)

Một số lỗi phát sinh khi chạy Fullnode

Can’t load AMD 64-bit .so on a AARCH64-bit platform

Lỗi này phát sinh do bạn đang sử dụng server có kiến trúc ARM, bạn phải chuyển sang kiến trúc x86.

This node does not support constant

Sau khi chạy fullnode xong, bạn sử dụng NodeJs để kết nối đến Fullnode thông qua thư viện TronWeb bạn nhận được thông báo lỗi như sau:

node:internal/process/promises:288
            triggerUncaughtException(err, true /* fromPromise */);
            ^

[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "this node does not support constant".] {
  code: 'ERR_UNHANDLED_REJECTION'
}

Node.js v18.17.1

Sau khi tìm kiếm tôi thấy lỗi này tương tự lỗi I query the USDT balance , but returned “this node don’t support constant”, ta chỉ cần đổi lại cấu hình sau trong tệp main_net_config.conf:

vm.supportConstant = true

HTTP method POST is not supported by this URL

Khi tôi dùng NodeJs gọi hàm tronWeb.trx.getBalance(“TWhDfwC8QE6pQyiYy248dNor3uphPEw5M2”) thì báo lỗi:

    data: {
      servlet: 'org.eclipse.jetty.servlet.ServletHandler$Default404Servlet-436d2bb9',
      message: 'HTTP method POST is not supported by this URL',
      url: '/walletsolidity/getaccount',
      status: '405'
    }

Lỗi này do tôi để địa chỉ SolidityNode giống địa chỉ Fullnode. Thực tế hai địa chỉ này ở 2 port khác nhau, chỉ cần sửa lại là chạy okie.

Không thấy có ví dụ lắng nghe event trên TronWeb

Mục tiêu của dựng Fullnode là để lấy thông tin giao dịch và blockchain nhanh nhất có thể. Nhưng khi tìm các API trên TronWeb thì không thấy có API nào liên quan tới việc subcribe và nhận event. Sau khi đọc tài liệu trên Event SubscriptionWebsocket Issue, mình hiểu thêm một số vấn đề như sau:

  • Trong cấu hình Fullnode, phần event.subscribe có tham số useNativeQueue:
    • useNativeQueue=false => Sử dụng Event Plugin mà cái plugin này sử dụng cho Java => Theo như hướng dẫn mình sẽ chạy EventPlugin này để đẩy event lên Kafka, sau đó app sẽ xử lý
    • useNativeQueue=true => Sử dụng một Message Queue nào đó
  • Trong Websocket Issue có phần source code ví dụ chạy trên NodeJs để nhận sự kiện, tôi đã tham khảo nó và phải sửa thêm do thư viện zeromq phiên bản mới nhất thì interface đã bị thay đổi.

Để mở Event thì ta phải thực hiện như sau:

B1: Thay đổi trên Fullnode

Trên Fullnode, sửa tệp cấu hình main_net_config.conf để mở một số event sử dụng:

Bật một số event quan trọng khi cấu hình

Sau đó ta chạy lại Fullnode nhưng phải thêm tham số “–es“:

java -Xmx24g -XX:+UseConcMarkSweepGC -jar FullNode.jar -c main_net_config.conf -d /home/ubuntu/tron-node/data --es

B2: Viết code phía client

Ở client ta viết code để nhận dữ liệu. Ta dùng NodeJs sử dụng thư viện zeromq@6.0.0-beta.19:

const zmq = require('zeromq');
const wssLink = "tcp://127.0.0.1:5555";

async function main() {
    const sock = new zmq.Subscriber;
    sock.connect(wssLink);
    sock.subscribe('blockTrigger');
    sock.subscribe('transactionTrigger');
    sock.subscribe('contractLogTrigger');
    sock.subscribe('contractEventTrigger');

    for await (const [topic, msg] of sock) {
        console.log("Received a message:", topic.toString(), msg.toString());
    }
}
main();

Chạy đoạn code trên bạn sẽ nhận được dữ liệu trả về. Đoạn code này tổng quan sử dụng lấy thông tin mọi event.

Đoạn code dưới đây chi tiết hơn sẽ nhận event và decode dữ liệu của một số event thông dụng. Riêng sự kiện Mint Burn của SunswapV3 các bạn tự decode nhé:

const TronWeb = require('tronweb');
const zmq = require('zeromq');
const wssLink = "tcp://127.0.0.1:5555";

function hexToAddress(hex) {
    if (hex && hex.startsWith("0x")) hex = hex.slice(2);
    if (hex && !hex.startsWith("41")) hex = "41" + hex;
    return TronWeb.address.fromHex(hex);
}

function decodeParams(names, types, data) {
    let hexData = data;
    if (data && !data.startsWith("0x")) hexData = "0x" + data;
    return TronWeb.utils.abi.decodeParams(names, types, hexData);
}

function decodeParameter(type, data) {
    let result = decodeParams(["name"], [type], data);
    return result.name.toString();
}

async function listenWs() {
    const sock = new zmq.Subscriber;
    sock.connect(wssLink);
    sock.subscribe('blockTrigger');
    sock.subscribe('transactionTrigger');
    sock.subscribe('contractLogTrigger');
    sock.subscribe('contractEventTrigger');

    for await (const [topic, msg] of sock) {
        // console.log("Received a message:", topic.toString(), msg.toString());
        let strTopic = topic.toString();
        if (strTopic=="blockTrigger") {
            let data = JSON.parse(msg.toString());
            let delayTime = (Date.now() - data.timeStamp)/1000;
            console.log(`[BLOCK}]: blockNumber=${data.blockNumber} - timeStamp=${data.timeStamp} - transactionNum=${data.transactionList?data.transactionList.length:0} - delayInSec=${delayTime.toFixed(2)} Secs`);
        } else if (strTopic=="contractEventTrigger") {
            let data = JSON.parse(msg.toString());
            let delayTime = (Date.now() - data.timeStamp)/1000;
            let rawData = data.rawData;
            if (!rawData) rawData = {};
            let topicMap = data.topicMap;
            if (!topicMap) topicMap = {};
            let dataMap = data.dataMap;
            if (!dataMap) dataMap = {};
            let eventTopic = (rawData.topics && rawData.topics.length>0?rawData.topics[0]:null);
            //console.log(`[${strTopic}]: blockNumber=${data.blockNumber} - timeStamp=${data.timeStamp} - from=${topicMap.from} - to=${topicMap.to} - eventTopic=${eventTopic} - delayInSec=${delayTime.toFixed(2)} Secs`);
            if (eventTopic=="ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef") {
                // TRC20: Transfer(address,address,uint256)
                let token = hexToAddress(rawData.address);
                console.log(`[EVENT]: ${token} => Transfer(from=${topicMap.from}, to=${topicMap.to}, value=${dataMap.value})`);
            }
        } else if (strTopic=="contractLogTrigger") {
            let data = JSON.parse(msg.toString());
            let topics = (data.rawData && data.rawData.topics?data.rawData.topics:[]);
            let eventData = (data.rawData && data.rawData.data?data.rawData.data:"");
            let hexAddr = (data.rawData && data.rawData.address?data.rawData.address:null);
            let eventTopic = topics[0];
            if (eventTopic=="cc7244d3535e7639366f8c5211527112e01de3ec7449ee3a6e66b007f4065a70") {
                // V1: Snapshot(address,uint256,uint256)
                let pool = hexToAddress(hexAddr);
                let operator = hexToAddress(decodeParameter("address", topics[1]));
                let trxBalance = decodeParameter("uint256", topics[2]);
                let tokenBalance = decodeParameter("uint256", topics[3]);
                console.log(`[EVENT] ${pool} => Snapshot(operator=${operator}, trxBalance=${trxBalance}, tokenBalance=${tokenBalance})`);
            } else if (eventTopic=="1c411e9a96e071241c2f21f7726b17ae89e3cab4c78be50e062b03a9fffbbad1") {
                // V2: Sync(uint112,uint112)
                let pool = hexToAddress(hexAddr);
                let result = decodeParams(["reserve0", "reserve1"], ["uint112", "uint112"], eventData);
                console.log(`[EVENT] ${pool} => Sync(reserve0=${result.reserve0}, reserve1=${result.reserve1})`);
            } else if (eventTopic=="c42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67") {
                // V3: Swap(address,address,int256,int256,uint160,uint128,int24)
                let pool = hexToAddress(hexAddr);
                let sender = hexToAddress(decodeParameter("address", topics[1]));
                let recipient = hexToAddress(decodeParameter("address", topics[2]));
                let result = decodeParams(["amount0", "amount1", "sqrtPriceX96", "liquidity", "tick"], ["int256", "int256", "uint160", "uint128", "int24"], eventData);
                console.log(`[EVENT] ${pool} => Swap(sender=${sender}, recipient=${recipient}, amount0=${result.amount0}, amount1=${result.amount1}, sqrtPriceX96=${result.sqrtPriceX96}, liquidity=${result.liquidity}, tick=${result.tick})`);
            } else if (eventTopic=="7a53080ba414158be7ec69b987b5fb7d07dee101fe85488f0853ae16239d0bde") {
                // V3: Mint(address,address,int24,int24,uint128,uint256,uint256)
                console.log("Mint Event", data.rawData)
            } else if (eventTopic=="0c396cd989a39f4459b5fa1aed6a9a8dcdbc45908acfd67e028cd568da98982c") {
                // V3: Burn(address,int24,int24,uint128,uint256,uint256)
                console.log("Burn Event", data.rawData)
            }
        }
    }
}

async function main() {
    listenWs();
}
main();

Bài viết này có hữu ích với bạn?

Kích vào một biểu tượng ngôi sao để đánh giá bài viết!

Xếp hạng trung bình 5 / 5. Số phiếu: 8

Bài viết chưa có đánh giá! Hãy là người đầu tiên đánh giá bài viết này.

Trả lời

Giao diện bởi Anders Norén